How do you debounce in JavaScript?

When working with resource-intensive tasks or server calls in JavaScript there is often a need for reducing how often those functions run. It is therefore great to know some patterns for handling those problems.

To debounce function calls, you can use the clearTimeout/setTimeout functions with the following steps:

  1. Cancel the previously running timer.
  2. Start a new timer.
  3. Run the delayed code on timeout.

In our example below we will initiate an HTTP search autocomplete request when the user pauses typing more than 300ms:

const search = document.createElement('input');
const suggestions = document.createElement('div');

// Declare timer
let searchTimer;
search.addEventListener('input', () => {

  // Clear timer
  clearTimeout(searchTimer);

  // Start a new timer
  searchTimer = setTimeout(() => {

    // Run slower code
    fetch(`/api/search?query="${encodeURIComponent(search.value.trim())}"`)
      .then((response) => response.json())
      .then((body) => {
        suggestions.textContent = body.join(', ');
      });

  }, 300);

});

search.type = 'search';
search.placeholder = 'Please enter your query...';
document.body.appendChild(search);
document.body.appendChild(suggestions);

Why do we need to debounce?

There are several input signals, especially in front-end JavaScript applications, usually in the form of events from various user inputs such as the user typing, mouse movements, and or resizing the browser window.

We need to debounce the more time-consuming functions, such as heavy computations or calls to server the side. This is to ensure that they do not trigger more often than the user's browser can manage, but also to reduce the number of server calls needed.

Another reason to debounce is that the updates of the user interface might become too frequent. This might create a flickering experience for example if areas of the page hides depending on the user's input where the value changes several times a second, such as when the user is typing. A good example of this could be validation error tooltips that should update on each user input, but still give a "flickering" impression.

How long should the debounce timeout be?

While it is hard to put definite numbers for how long a debounce delay should be, here are some rules of thumb that depend on the use case.

Debounce timeout for fluid updates

When working with that needs a fluid and smooth user experience a delay of about 30 milliseconds is a good balance. A game running on 30 frames per second is still fairly smooth and would give us 33.33 milliseconds for each frame (1000 / 30 = 33.33).

Delaying no more than 30 milliseconds is usually barely noticeable but still greatly debounces things like window resize or scroll events. These debounce delays can therefore be added fairly freely to your code without having too much negative impact on your application.

Debounce timeout for keyboard input or window resizing

For both keyboard input and window resize events a delay of 300 milliseconds is a good balance. This delay will be visible so the user will feel experience some amount of lag in the user interface.

For cases like the one above where you want to debounce autocompletion to run only when the user stops typing for a short while, to save server resources, this long delay is usually fine, and using shorter delays would trigger searches too frequently while the user is still actively typing.

Do note though that the total time to present the suggestions will be total time = debounce delay + server latency + server database lookup, so adding to long debounce delay here will worsen the user experience.

Another good use case for 300 millisecond delays is when the user is dragging to resize the browser window which will trigger a huge number of events and will often trigger many reflow operations in the browser due to layout shifts. Here a 300 millisecond delay will prevent the app from locking up while the window is being resized IF there are a lot of calculations in your application related to the window size. Also, the user will expect some delay and redraw on a page resize so it does not hurt the user experience that much.

That being said, for window resizing you should always try to solve these problems with CSS rather than JavaScript as that will always produce a smoother user experience, and can often be solved with much less code. Features like media queries of layouts with CSS grid, or flexbox can often be of help in those cases. At times though some of these problems will still require JavaScript.

How do I create a reusable debounce function?

Adding clearTimeout/setTimeout pairs throughout the code is sometimes good enough, but it might also be nice not having to keep track of the timer variables.

They can therefore be kept inside a function scope:

function debounce(callback, delay) {
  let timeout;
  return (...args) => {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => callback.apply(context, args), delay);
  };
}

Using the debounce function the first example could then be written as the shorter:


const search = document.createElement('input');
const suggestions = document.createElement('div');

search.addEventListener('input', debounce(() => {

  fetch(`/api/search?query="${encodeURIComponent(search.value.trim())}"`)
    .then((response) => response.json())
    .then((body) => {
      suggestions.textContent = body.join(', ');
    });

}, 300));

search.type = 'search';
search.placeholder = 'Please enter your query...';
document.body.appendChild(search);
document.body.appendChild(suggestions);

Now you can keep your code delayed but still more performant!