Throttle vs Debounce on real examples
What's the difference between throttle and debounce? Both are timing functions that limit the number of function calls so
finding a difference can be challenging. It is for me, more often than I'd like to admit. So I decided to write down a
couple of examples. For which interactions we should use the throttle and for which debounce?
As a diligent student of User Interactions, I'm more interested in use cases than theory. By reading this post you'll
also learn when not to use none of them because we have better APIs. Interactive examples included! 😍
But first, let's start with a basic question:
What's the purpose of using these timing functions?
The ultimate reason is the performance and protection of resources. That's it. The effect it has on UX is rarely the thing that we wanted to do. We have to deal with the consequences of using these functions but if I could, I'd not use them at all. But we don't have unlimited resources so we have to protect them.
Throttle and Debounce let you call a function fewer times than it would be usually called in a storm of events. Fewer things to process simply mean better performance.
Throttle runs a given function just once in a given period. Say you've got 400 events in 2 seconds but you've decided to throttle that stream and let it be executed just once per second. As a result, your function will be called just twice. Instead of 400!
From a user's point of view, their action will be run every 1 second. No matter how many actions in-between they do.
Debounce on the other hand will run a given function after a given period of time will pass without a single event. To put it to the extreme - if you have a constant stream of events that happen every 1 second, and you decided to debounce a function to be run after 2 seconds of "no action" then your function will be never executed.
From a user's point of view, they have to stop doing something for 2 seconds to see the result of their action.
When we should use one or another?
Maybe instead of me saying what I think we could take a look at some real-life examples with three cases:
- no timing function
and we will get to the answer together by looking at a number of events handled? and the general look&feel?
Searching or typeahead
In functionality like searching or getting hints (typeahead) in most cases, we have to call the server. This kind of functionality happens on every keystroke - not by clicking on the "Search" button. So it will naturally create a lot of events.
We have to protect the server and reduce the number of calls. Play with the input below. Just for the sake of simplicity,
this search will always show a loading state and "No results".
Observe the number of API calls:
Did you notice that? Debounce seems to be the best in protecting the resources. It waits until a user stops typing.
I know everyone is referring to this example as the perfect use of
debounce but that's not perfect. It will make your
search feel a bit sluggish. No matter how fast your API is, it will always add that extra delay until the API call will
be made (here 400ms). There is a better way for typeahead but I'll write about it in another post. Here let's focus on
timing functions. If you need to aggressively protect the resources I say debounce is good enough.
Save as a user writes
So is debounce the best for typing activities? No. Not always. Imagine you have a rich text editor where a user types something and we perform autosave in the background. Debounce's characteristic to wait until the stream of events has stopped for a while can be dangerous here.
We can imagine that an impatient user types something and leaves the page before the actual save was performed. With autosaving, we have to use every occasion to store the state (but still protecting our resources).
Play with the textarea below and observe what were the saving states. The server you use can also keep these intermediate states as points in history to offer a simple revision functionality.
Hover on the circles that represent the saved state (they will appear after typing). Using
throttle seems to be a
better idea here. We do want to protect the server (especially here!) but we don't want to lose any of the user's work either.
Timing functions are set to something long (like 2-3 seconds) to not produce too many saves but that harms
It is rarely called and doesn't give us "checkpoints" when typing.
It's not yet ideal but a very good start. To make it better you should learn about canceling requests and leading or trailing edge of a timing function. We'll get back to it. But for now, I say throttle is the way to go for autosaving.
One of the most common examples in timing functions is limiting the number of scroll events. Let's look at how many events scroll can generate:
Okay, so it seems we should do something about it. Scroll naturally generates a massive amount of events. So what kind of interactions we should consider limiting? I know there are examples of using throttle or debounce to calculate the scroll position to implement an infinite scroll. That's wrong. You should use Intersection Observer for that. Period.
But we can imagine you'd like to show the progress of scroll a.k.a "how much did user read" functionality. Let's see how that works with three approaches (events count is in the brackets):
So we are updating the style of progress bars depending on the scroll position. It's obvious that timing functions introduce some
kind of lag here.
debounce here is something hard to consider unless you really need to protect resources but why would you
implement such a feature then?
throttle is a bit better to reduce the number of events and still looking quite ok. But to have it really "smooth" we would have
to go down to
16ms of delay. Why 16ms? Because that often you have to refresh an animation to keep
60FPS (1000ms / 60 frames).
There is a better API for that and is called
Play with scroll below. Observe how smooth the animation is and how often it was refreshed.
Boom 💥 If you are on a fast computer (MacBook?) you won't even notice a difference between "no timing function", "requestAnimationFrame"
and "throttled run every 16ms". But trust me, use
rAF in case you animate something based on scroll or mouse move.
Here is a screenshot of one of the states I landed in:
On my super-fast computer, the difference between
throttle run every 16ms and
requestAnimationFrame was super small
and you'd not notice the missing frames in the animation. It can make a difference on slower devices.
Leading and trailing edge
So far, the examples above were using the trailing edge. The edge settings tell if the function should be run at the beginning of the stream of events or the end.
For throttle, the case is easy when picking leading-edge - you want your function to be run immediately after the stream of events occurs and then every 1s for example.
Debounce on the other hand changes its characteristic. The mental model for running debounce immediately is to think about it as a mute. You run something once and ignore all other events until there is a time window (say 1s) of no events. If the events happen after that we are back to running it only once at the beginning.
How to implement debounce and throttle? By importing it from lodash for example 😛 Really. There is no need to
re-implement the wheel. Their implementation is great and has additional features
that I didn't cover here. Like
maxWait. A super-powerful feature that will let you call a function if it didn't happen
for some time (emergency call!). Really great to fight debounce's slugginess.
The timing functions are great. But for some interactions, we have better APIs these days. Sometimes you might not even see the UI affected, and that's generally what we want.
Protection of resources is important but remember - there is a user at the end. The last thing we want is to screw up their experience.
By adjusting timings you can eat a cake and have a cake. Bon appetit! 🍰