Debounce and Throttle: a visual explanation
Debounce and throttle are two concepts that we can use in JavaScript to increase the control of our function executions, specially useful in event handlers.
Both techniques answer the same question "How often a certain function can be called over time?" in different ways:
- Debounce: Think of it as "grouping multiple events in one". Imagine that you go home, enter in the elevator, doors are closing... and suddenly your neighbor appears in the hall and tries to jump on the elevator. Be polite! and open the doors for him: you are debouncing the elevator departure. Consider that the same situation can happen again with a third person, and so on... probably delaying the departure several minutes.
- Throttle: Think of it as a valve, it regulates the flow of the executions. We can determine the maximum number of times a function can be called in certain time. So in the elevator analogy.. you are polite enough to let people in for 10 secs, but once that delay passes, you must go!
Event handlers without debouncing or throttling are like one-person-capacity elevators: not that efficient.
I hope this bad analogy helps, but words some times don't help much to grasp these concepts, so I created a demo to understand throttling and debouncing visually, applying them to the mousemove
event.
Implementation
function elevator_departure(name){ alert(name + " was the last one. Nobody else? Let's go then");};var debounced_elevator_departure = $.debounce(200, false, elevator_departure);debounced_elevator_departure('John');debounced_elevator_departure('Mike');debounced_elevator_departure('Peter');// You will see *only* one message, "Peter was the last one. Nobody else? Let's go then";
I've found these 3 popular implementations of debouncing and throttling in JavaScript. I really recommend you to read first the Ben Alman's description to understand better debouncing (and its inmediate flag) and throttling (and its trailing flag).
The underscore and lodash are implemented differently, but covering the same options (except throttling without trailing).
- Underscore.js by Jeremy Ashkenas *
- Lodash.js by John-David Dalton *
- jQuery Plugin by Ben Alman.
* Update: : John-David told me that Ben Alman helped him in his _.throttle implementation. And also notice that after my bug report, underscore.js has started to use the same implementation of throttle as lodash.js.
Here is the visual demo comparing the jQuery Plugin with underscore/lodash libraries. I hope it's self-explanatory (tell me if it is not):
Notes:
- The source code of this demo is hosted in github. This is a a screenshot.png of what you should see.
- If you see that in your browser the animation is not smooth, open the demo in a separate page, or try it in Chrome.
- I've demo it in Android, tapping in the mouse area..
- Each cell represents about 30 ms, but JS Interpreters are single threaded, so browsers are not that precise. This demo is not trying to be a "perfect" representation, just enough good to grasp what's going on.
- When you do a
setTimeout
, the time resolution differs in each browser between 4ms and 15ms (Nicholas Zakas article), even asetTimeout(fn,0)
it will take at least 4 ms. In the other hand, Date has a 1 ms timer resolution. - The
mousemove
events in the first row are actually throttled also (to 80ms). It's the only way I could manage to be able to paint all of them and make the demo useful. - To get the trailing option in
$.throttle
, you need to passfalse
for no-trailing. Don't you hate negated flags? - Don't forget that all these are builders, they return a function, so you just need to build once your debounce'd function.
Use cases for debounce
Use it to discard a number of fast-pace events until the flow slows down. Examples:
- When typing fast in a textarea that will be processed: you don't want to start to process the text until user stops typing.
- When saving data to the server via AJAX: You don't want to spam your server with dozens of calls per second.
Use cases for throttle
Same use cases than debounce, but you want to warranty that there is at least some execution of the callbacks at certain interval
- If that user types really fast for 30 secs, maybe you want to process the input every 5 secs.
- It makes a huge performance difference to throttle handling scroll events. A simple mouse-wheel movement can trigger dozens of events in a second. Even Twitter had once problems with scroll events, so learn from others mistake and avoid this easy pitfall.
Minor bugs in _.throttle (FIXED)
Update. Ignore this section. Both libraries underscore and lodash.js master branches have been fixed in less than 12 hours, wow! 24h later lodash.js included the fix in the stable release 0.8.2. I will update this post again when the fix go to the stable release of underscore.js.
Although this post was at the beginning just a visual experiment of debounce & throttle functions, I saw some wrinkles that need to be ironed in underscore and lodash.
I will update this post as soon as they are fixed, no FUD intended here.
First, I found a edge-case bug in the _.throttle function of underscore.js. Visually in the demo looks like this:
In certain cases, it will skip a execution and waits more (double wait
time) than necessary. I opened an issue in github Update: Bug fixed. Underscore.js master branch has been updated to the same implementation as lodash.js.
This bug is not present in lodash.js, because it has a different implementation of throttle. But before John runs to tweet about it ;), I'm afraid that I found also a different bug in lodash implementation. Visually in the demo looks like this:
In certain cases, due of not clearing the setTimeout
, it's leaving a tiny window of time opened, allowing a extra execution.
More details in the github issue queue. Update. 2 hours after reporting the bug, this issue has been fixed in the master branch of lodash.js, and the fix is part of the last stable release 0.8.2.
jQuery's guid
Finally, exploring the source code of Alman's plugin, I've found that it takes care of assigning the same guid
of the original function to the wrapper function. It seems the integer guid
is an internal jQuery parameter that will help later to unbind correctly the function. The lack of support for this feature in lodash/underscore makes me wonder if it could have any consequence, specially if you want to use them in single-page apps with Backbone.js. I will try to dig more into this and update soon.
Improving Drupal
This post is in the Drupal Planet for a reason: wouldn't it be great to have these helpers in Drupal 8 core? I opened a feature request. I've located some comments of _nod and Kiphaas7 and jessebeach already thinking about it.
And also check this issue: add underscore.js & Backbone.js to Drupal 8 core, that looks very promising. My opinion in #75.