


Debouncing in Javascript
In 2011, an issue popped up on the Twitter website: when you were scrolling down your Twitter feed, it became slow and unresponsive. John Resig published a blog post about the problem where it was explained how bad of an idea it is to directly attach expensive functions to the scroll
event. The suggested solution by John (at that time) was a loop running every 250ms, outside of the onScroll event
. That way the handler is not coupled to the event. With this simple technique, we can avoid ruining the user experience. These days there are slightly more sophisticated ways of handling events like Debounce, Throttle etc.
So what exactly is Debounce ?
This function is built in order to limit the amount of times a function is called — scroll events, mousemove events, and keypress events are all great examples of events that we might want to capture, but can be quite taxing if we capture them every single time they fire. In order to combat this, we implement debounce and throttle functions. We won’t discuss the throttle function in this post, but a debounce function will wait until the last time the function is called and then fire after a predetermined amount of time or once the event firing becomes inactive.
For those in camp TL;DR, here’s a demo!
See the Pen Mouse/Touch Move Debounce & Throttle visualisation w/ React + Pixi 🤓👀 by Jhey (@jh3y) on CodePen.
A debounce is a higher-order function, which is a function that returns another function (named executedFunction
here for clarity). This is done to form a closure around the func
and wait
function parameters and the timeout
variable so that their values are preserved. The following is a definition of each variable:
func
: The function that you want to execute after the debounce timewait
: The amount of time you want the debounce function to wait after the last received action before executingfunc
. For our typeahead example, it would be the amount of time to wait after the last key press.timeout
: The value used to indicate a running debounce.
We can use a debounce doing:
var returnedFunction = debounce(function() {
// All the taxing stuff you do
}, 250);
window.addEventListener('resize', returnedFunction);
Since debounce returns a function, the executedFunction
from the first example and the returnedFunction
function from the second example are the same function. Every time the window is resized, it will execute executedFunction
/returnedFunction
.
Our executedFunction
spreads over the parameters (...args)
to allow for the debounce function to receive any number of parameters to pass to the callback.
We declare a callback function named later
which is the function that’s executed after the end of the debounce timer. This is what will be called after the setTimeout
expires.
Next, we clearTimeout
which had prevented the callback from being executed and thus restarts the debounce. Then we (re-)declare timeout
which starts the debounce waiting period. If the full wait
time elapses before another event, then we execute the later
callback function. The timeout
is set to null
which means the debounce has ended. This executes func(...args)
.
There is a more advanced version of this where we can pass an immediate
flag to debounce
. Currently we always wait until the end of the debounce to execute the callback, but with immediate
, you can change it such that the function executes at the leading edge and won’t allow you to execute again until it has delayed calling long enough to deplete the timer.
Here’s a commented version of the function as well.Here’s a commented version of the function as well.
// Originally inspired by David Walsh (https://davidwalsh.name/javascript-debounce-function)
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// `wait` milliseconds.
const debounce = (func, wait) => {
let timeout;
// This is the function that is returned and will be executed many times
// We spread (...args) to capture any number of parameters we want to pass
return function executedFunction(...args) {
// The callback function to be executed after
// the debounce time has elapsed
const later = () => {
// null timeout to indicate the debounce ended
timeout = null;
// Execute the callback
func(...args);
};
// This will reset the waiting every function execution.
// This is the step that prevents the function from
// being executed because it will never reach the
// inside of the previous setTimeout
clearTimeout(timeout);
// Restart the debounce waiting period.
// setTimeout returns a truthy value (it differs in web vs Node)
timeout = setTimeout(later, wait);
};
};
Common scenarios for a debounce are resize
, scroll
, and keyup/keydown
events. In addition, you should consider wrapping any interaction that triggers excessive calculations or API calls with a debounce.