#16 Programming
23 mins   /

What exactly is debouncing in JavaScript?

Explore the origins of debouncing, its adaptation into software engineering, and how to implement it in JavaScript

As a web developer, performance optimization is not just a nice thing for you to know but rather a must. No second guesses. The web is a fragile medium. Bad performance could quickly backfire and leave the user of the web app in dismay.

Performance optimization is such a large and complex topic that there could be a whole book on it (and there even are many). Countless of ways exist for optimizing the performance of a webpage. When we talk about specifically optimizing the performance of JavaScript in the webpage, again there are numerous ways.

One that you shall learn in this article is debouncing. Let's get into it.

Debouncing in electrical engineering

Debouncing is originally a concept in electrical engineering. It helps to understand the whole intuition behind it in this field and then relate it back to software engineering (and JavaScript).

At the core, there is an issue studied in electrical engineering. That is, when a push button (any kind) is pressed, instead of generating one push signal, as we typically expect, it produces many consecutive push signals before coming to a final, resting position.

Imagine dropping a ball on the floor. As the ball hits the floor, it doesn't immediately come to a rest, does it? It bounces off the floor to a lesser height than before, then falls back again, then bounces off again, and so on and so forth.

This constant back and forth motion of the button (or of the ball) before coming to a final, resting position is referred to as bouncing.

Bouncing in buttons has the undesirable consequence that one button press causes multiple, weaker and weaker signals. The circuitry, without any remedy, doesn't treat these signals any differently from the initial press but rather as normal signals too.

The ultimate result is that during this phase of back and forth button oscillations, the circuitry completes and then breaks, causing swift and short-lived on/off signals in connected appliances. This sequence of fluctuations often tends to deteriorate electronic appliances or lead to falsey outcomes, and so we must rectify it.

In simple words: As soon as we perform an action (a button press), we get a quick burst of related actions (weaker presses), i.e. bouncing, that we do NOT want to respond to.

The remedy is to prevent the bouncing, or as it's more commonly known, to do debouncing . (And now it all makes sense, doesn't it?)

Debouncing in electrical circuitry is performed using different kinds of components — RCs, Schmitt triggers, flip-flops. It ensures that only one signal ever reaches the end appliance for one button press. Thanks to debouncing, any signals generated immediately following an initial button press are effectively canceled out.

Adaptation in software engineering

The concept of debouncing has been extended and adapted accordingly into software engineering, especially in event-driven programming, where we regularly deal with events.

In event-driven programs, it's often the case that we have certain events that intrinsically fire at a rapid rate, for e.g. a key held down, sending rapid key-press events, or the mouse being moved, sending rapid mouse-move events, etc.

These rapid events assimilate the "bouncing" described above in that they are something that we want to ignore for the end program.

The fix to avoid these rapid events from triggering callbacks in the application we do... you got it... debouncing. But how? By introducing a short delay, typically a fraction of a second, during which any event is ignored. Only once this delay ends without any event firing in it is any subsequent event considered.

For example, let's suppose a debounce delay of 100ms for key events. If a key on the keyboard is pressed quickly twice, with an interval of 80ms in between the presses, we'll ignore the second event because it fired within the 100ms mark.

Debouncing in JavaScript

Debouncing certainly wasn't pioneered by JavaScript; other languages implemented it in the past before JavaScript, especially those dealing with event-driven programming and GUI development.

But probably because of the unrivaled popularity of JavaScript and the fact that it also works around an event-driven programming model, debouncing became a well-known concept amongs developers. Let's consider a quick example of debouncing in action in JavaScript.

Say we have a search bar on a 3D modeling software's documentation page to search into the entire documentation (which is quite huge).

As soon as a character is input into the search field, the query made so far is submitted to the server and a list of suggestions is returned back and presented to the user.

This behavior of trying to suggest best matches for the user's query is often called auto-suggestion or autocompletion.

Now suppose the user enters the text "grid" to find out resources related to the sophisticated grid feature of the 3D software.

Of course, at the end, having entered the entire query "grid", the matching suggestions are shown to the user. But most importantly, as each letter of the word is entered, a request is sent to the backend for the suggestions pertaining to the query formed thus far.

You can rightly assume the search input to be implemented something like this:

JavaScript
searchInputEl.addEventListener('input', function (e) {
   getAndDisplaySuggestions(e.target.value);
})

Upon the dispatch of the input event on the input field, the matching suggestions are retrieved from the server and then displayed.

This works but there's a big opportunity for improvement.

Instead of submitting the query to the backend on every keystroke, we could introduce debouncing to do so only once the keystrokes end.

For example, if we press G and R very quickly, one after another and then take a long pause, only a single request would be sent for the query "gr" instead of two discrete requests (one for "g" and one for "gr").

In the amended code below, we introduce debouncing in the event listener, with a delay of 300ms:

JavaScript
let timerId = null;

searchInputEl.addEventListener('input', function (e) {
   clearTimeout(timerId);
   timerId = setTimeout(() => {
      getAndDisplaySuggestions(e)
   }, 300);
})

This is a typical implementation of debouncing in JavaScript. It consists of three parts:

  • A variable holding an ID for controlling the timer set up using setTimeout().
  • A call to clearTimeout() to restart the delay-counting timer if the underlying action (in this case, an input event) happens before the given debounce delay.
  • A call to setTimeout() to instate the timeout.

The 300ms delay here effectively prevents the "bounce" (quick burst of actions) of the input event from being taken into consideration with respect to the final outcome that we want to achieve, i.e. getting suggestions for the query from the server and then displaying them.

Debouncing here offers us two benefits:

  • Firstly, the server doesn't get overloaded with unnecessary requests.
  • Secondly, the bandwidth of the user is preserved. In general, the lesser the number of requests, the lower the bandwidth.

Thanks to debouncing, our definition of an "input" in this example effectively changes. That is, now an input is an entry of data that doesn't have another "bounce" within 300ms. So if, let's say, you enter two characters very quickly, one after another, the program treats it as one single input.

Ain't that amazing?

A general debouncing utility

Suppose you have a function fn that's being invoked very quickly and now you wish to debounce it. Also suppose that there is a general debouncing utility that takes your function fn and returns another function that is a debounced version of this function.

That is, this utility is basically a higher-order function, let's call it debounce(), that takes in fn and the debounce delay as arguments, and then returns a debounced function wrapping fn.

How would you define this higher-order function in JavaScript?

Here's the definition of this function:

JavaScript
function debounce(fn, delay) {
   let timerId;
   return function (...args) {
      clearTimeout(timerId);
      timerId = setTimeout(fn, delay, ...args);
   }
}

Notice the usage of ...args as a parameter of the returned function and as an argument to setTimeout(). This makes sure that whatever arguments are provided to the returned (debounced) function get relayed back to the original function as they are.

Being a bit more logically strict with the code, we get the following — making sure that clearTimeout() is only called when there is an ongoing timer:

JavaScript
function debounce(fn, delay) {
   let timerId;
   return function (...args) {
      if (timerId) {
         clearTimeout(timerId);
      }

      timerId = setTimeout(() => {
         fn(...args);
         timerId = null;
      }, delay);
   }
}

With this utility in place, we can rewrite the input event listener presented earlier as follows, with a delay of 300ms:

let debouncedGetAndDisplaySuggestions = debounce(getAndDisplaySuggestions, 300);
searchInputEl.addEventListener('input', function (e) { debouncedGetAndDisplaySuggestions(e.target.value); })

First, we obtain a debounced version of getAndDisplaySuggestions() by providing it to debounce(), with a delay of 300ms. Then, we call this debounced function within the input listener.

This is the same thing as manually setting up the entire debouncing logic in the input listener as we did above, just that now the debouncing has been generalized into a helper function.

Things to note regarding debouncing

While debouncing is a really useful optimization skill to have in one's arsenal, it's crucial to be aware of two things: how to use it properly and when it isn't necessary.

Choose a debounce delay wisely

First things first, the debounce delay must strike a balance between too less and too much. Too less and you risk losing the benefits of debouncing. Too much and your users will start to notice considerable delays in interaction.

So how to choose a debounce delay wisely? Well, it's purely trial and error.

Come up with a rough value, obviously one that is practical, and then test the debounced implementation. If the delay feels too less, increase it. If it feels too much, decrease it. Repeat until you get to the perfect point.

A good starting point would be 100 to 300 milliseconds.

The debounce delay obviously depends on the event being debounced but usually it tends to fall below a third of a second.

Make sure that debouncing really helps

Just because the keydown event in JavaScript fires rapidly doesn't mean that we must always debounce it. In fact, this is far from good programming to debounce unnecessarily everything.

For example, consider an autocompleting input field, similar to the one we discussed above, this time to enter a country name from a 100 choices. If the entire dataset of the countries resides on the client. it wouldn't make much sense, if at all, to debounce the keydown event and show suggestions for countries after a delay.

Rather, we're absolutely fine at keeping up with the event's normal pace since the autocompletion logic doesn't involve a network roundtrip and it's also just a matter of iterating over a few elements. Without debouncing, the interaction of the autocompleting input field would feel superbly crisp.

Imagine debouncing in this setup — it's completely unnecessary and will destroy the sheer smoothness of the autocompleting input.

Bilal Adnan

Hi there! 👋 I'm the founder of Codeguage — basically the guy who's trying to make life easy for self-taught devs. You can follow me on LinkedIn or Medium to stay up-to-date with my conversations.