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 time
  • wait: The amount of time you want the debounce function to wait after the last received action before executing func. 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 resizescroll, and keyup/keydown events. In addition, you should consider wrapping any interaction that triggers excessive calculations or API calls with a debounce.

Leave a Comment