Forever Functional: Debouncing and throttling for performance
source link: https://blog.openreplay.com/forever-functional-debouncing-and-throttling-for-performance
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
In many cases, you want to limit how often a function is called. Implementing such limits can lead to complex code, unless a functional technique is used. In this article, we’ll show how we can debounce and throttle functions (don’t worry! we’ll explain this below!) by using higher order functions. Applying those techniques will give your code an immediate performance benefit, so they are good tools to know!
Debouncing? Throttling? Sounds dire…
Debouncing and throttling are two techniques that limit when and how often a function is called. There’s an interesting symmetry:
- debouncing a function means we wait a certain time, doing nothing, until we call the function.
- throttling a function means we wait a certain time, doing nothing, after we call the function.
In both cases, we won’t be calling the function in the “do nothing” periods. We’ll show practical use cases for both situations below.
Before starting with the new algorithms, it’s interesting to remember function memoization from [a previous article in this series]. With memoization, you modify a function but more drastically: it will get called just once and never more. With debouncing and throttling, we don’t want to go that far: we wish to allow a function to do its thing again — but we’ll be restricting when this will happen, implementing some timeout between calls.
Debouncing functions
The concept of debouncing comes from electronics and involves waiting to activate something until specific stability has been reached. Let’s have an everyday example for web apps: an autocomplete component. As the user types letter after letter, you would have to call a function to query an API to fetch possible options. However, you don’t want to do it keypress by keypress, because there will be a lot of calls, and what’s worse, you won’t have any use for many of them — you’ll only care about the last one. The idea, then, is to debounce the calling of the function. You may call it as many times as you wish, but no actual API calls will go out until there has been some time without any further calls. This concept also works with events handlers: dealing with scroll or mouse movement events may quickly cause performance issues because the corresponding methods run very frequently. However, if you debounce the handlers, they will only run when there’s a lull in events, avoiding jittery or “stuttering” effects. The following diagram explains the concept.
Orange marks represent events. After each event, a waiting period (in light green) starts, but it is interrupted by any new event. If the waiting period is completed with no interruptions, then the function is called (dark green). There may be any number of events, but the debounced function will run only after a specified pause. Let’s see how to do this in JavaScript.
When you debounce a function, you get a new one. This new function can be called as many times as you wish, but it will set a timer with a timeout. Every time you call the new function, the timer gets reset, and the timeout starts again. When the timeout is done (meaning, there were no calls for a time) the original function will get called. Simple!
An example follows. We have a doSomething(...)
function that just logs its argument. We’ll debounce it so it will only run if half a second (500 milliseconds) has elapsed since the last call. We will have a loop calling the debounced function repeatedly, with a timed wait between calls. (We’ll use a sleep()
function for those delays.) Usual delays will be short, 200 milliseconds (so less than half a second). Twice in the loop (at the 13th and 15th passes), we’ll have longer delays (750 milliseconds), enough for the timeout for the original function to be over. After the last pass of the loop is done, the timeout runs out, and the original function is called for the last time.
When we run this code, we get the following result:
The original function (the one that prints “DOING SOMETHING”) was only called three times: at times #13 and #15, when a long enough delay was provided, and at the end of the loop when no more calls were forthcoming. We achieved the debouncing requirement very easily. In an autocomplete component, we’d just call the debounced function for each keypress — but no actual calls to the API would be made unless the user stopped typing for a while.
Throttling a function
There’s a different situation that we also want to control. Imagine you have a form in which you enter some id, and when you click on a RETRIEVE button, an API call is made to a server to retrieve some data matching that id. If the user starts clicking on the button, again and again, lots of calls will go out — but they will most probably receive the very same results. You could want to throttle the calls to the API so that the first call would go through, but then, for a certain time, the call wouldn’t be repeated. Similar problems could happen in games: what if the player repeatedly clicks on the “fire” button, even if there’s a cooling time between shots? Yet a third case: when implementing ”infinite scrolling” you want to trigger the request for more data when you are scrolling down, but you don’t want to check very often — and you don’t want to wait until the user reaches the bottom (as with debouncing) because that would be too late!
As we mentioned earlier, debouncing and throttling are similar - but with the former, you wait until doing something, and with the latter, you do something but wait until doing it again. The following diagram shows the difference.
As with debouncing, events are marked as orange rectangles. When an event occurs, we instantly call the throttled function if we are not in a waiting period. However, if we happen to be in the timeout zone, we don’t do anything; we ignore the event. No matter how frequently events occur, the throttled function will limit the number of calls.
How does this work? So, we’ll have code that’s similar for both. When you throttle a function, you get a new one. This new function, when called, will call the original function immediately, but it will set up a timer with a timeout. Whenever the function gets called, it will first check if the timer is active; if so, it won’t do anything. When the timeout ends, the timer will be cleared, and calls to the original function will be allowed again.
Let’s try the same example from the previous section but apply throttling instead. We’ll use the same function but throttle it to a call every 3000 milliseconds, three seconds.
If we run this loop, results will logically vary compared to debouncing.
The very first time that the new function got called, it did call the original one. Afterward, there were many more calls, but the original function didn’t get called again until three full seconds passed (in call #14). The same happened after call #27 because at least three seconds had passed since the previous call to the original function. No matter how often you call the throttled function, the original function will only get called once every three seconds.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Throttling promises
Let’s now consider a last case. We may be calling an external API, but we don’t want to call it over and over again with the same parameters. (A simple example: you may call a weather API, and the provider only updates its data every 5 minutes, so you don’t want to call it more frequently.) If we wanted to just call the API once and never call it again, we could do with promise memoization, as we saw in a previous article, but here we want calls to eventually go out again, after some time. We can marry promise memoization and throttling, and the following would be an implementation.
Most of the code is exactly as in the previous article, with three differences:
- at (1), we define a
timers
object because we will need many timeouts, one per promise. We don’t want to throttle all calls, only repeated ones. - at (2), we check if a timeout already exists for this promise. If not, we set it up to run and delete both the cached promise and the timer after some time.
- at (3), we delete both the cached promise and the timer (if any) in case of a problem with the API call.
Let’s see how this works. I’ll use the OpenWeather API to get the temperature for a city. Getting the temperature requires the following code; note that you’ll have to get an API key of your own. I added console.log()
calls to help understand how this works; in a real app, you wouldn’t have that!
Now we can do a test, repeatedly calling the API to get the temperature at Montevideo, Uruguay (my home city), and Pune, India (where I also lived for a time).
I throttled the promise to only go out every 3 seconds; you would use a more extended time delay for a real app. The output of this loop is as follows.
Some details:
- the first call (for Montevideo) goes out at the first step; the results come back soon afterward
- the following calls for Montevideo do not produce a call
- only after 3 seconds have passed, a new call for Montevideo goes out at step #10
- when we call the API to get Pune’s temperature at step 13, the call goes out immediately, as with our first call for Montevideo
- a second call for Pune’s temperature, at step 15, doesn’t go out at all
With this mechanism, we avoid repeating calls, effectively throttling the promise.
Conclusion
With the techniques shown in this article, you can limit the frequency of calls to a given handling function, avoiding delays, stutters, and plain overloading of remote servers. The techniques are easily implemented (and libraries such as LoDash and Underscore provide such implementations) so these tools are valid ones for you to use!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK