Table of contents
- Debouncing in electrical engineering
- Adaptation in software engineering
- Debouncing in JavaScript
- A general debouncing utility
- Things to note regarding debouncing
As a web developer, performance optimization is not just a nice thing for you to know but rather a clear-cut must. No second questions in that. The web is a fragile medium. Bad performance could quickly — or perhaps, very 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). There are numerous ways of optimizing the performance of webpages, one of which that we shall cover here is debouncing.
In this article, we'll get to learn what exactly is debouncing in JavaScrip; where did the concept originally come from; why we need it; how to implement a general debouncing utility; and some important things to note when debouncing.
Let's get into it.
Debouncing in electrical engineering
Debouncing is originally a concept from electrical engineering. It helps to understand the whole intuition behind it in this field and then relate it back to software engineering.
At the core, the issue is that 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 of course doesn't immediately come to a rest; rather, 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 ball, and even of the button, 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 here (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 commonly known, to implement debouncing.
Debouncing in electrical circuitry is performed using different kinds of components — RCs, Schmitt triggers, flip-flops — and 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 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 form something that we want to ignore for the end program.
A simple fix, i.e. debouncing, is to introduce a short delay, only a fraction of a second, during which any event is ignored. Only after this delay passes without any event firing is any subsequent event considered.
For example, 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 given that we've asserted that there must be a delay greater than 100ms in order for any subsequent event to be considered as discrete.
Debouncing in JavaScript
Debouncing certainly wasn't pioneered by JavaScript; other languages have implemented it in the past before JavaScript, especially those that deal with event-driven programming and GUI development.
But probably because of the widespread use of JavaScript, and the fact that JavaScript also works around an event-driven programming model, debouncing became a well-known concept amongst developers.
Let's consider a quick example of debouncing in action in JavaScript.
Let's say we have a search input field 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 this input field, the user is provided with a list of suggestions. The entered data is submitted to the server and the resulting suggestions are returned back in the response.
💡 Notice: This behavior of trying to guess, or automatically complete, what the user is trying to search for is often called autocompleting.
Now suppose the user enters the text 'grid' to find out more about the sophisticated grid feature of the software.
Of course, at the end, having entered the query 'grid', the matching suggestions are shown to the user. But most importantly, as we enter each letter of the word, a request is sent to the backend for the suggestions pertaining to the query thus far.
Something like the following:
searchInputEl.addEventListener('input', function (e) {
getAndDisplaySuggestions(e.target.value);
})
Upon the dispatch of the input
event on the search input field, the matching suggestions are retrieved from the server and then displayed on the page.
This works but there's an opportunity for improvement.
Instead of submitting the query to the backend on every keystroke, we could rather introduce debouncing to let go off the quick, successive keystrokes.
For example, if we press 'g' and 'r' very quickly, one after another, a single request would be sent for 'gr' instead of two discrete requests for 'g' and 'r'.
In the amended code below, we introduce a delay of 300ms before submitting the request for suggestions to the backend, with the help of setTimeout()
:
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 a timer, the one set up using
setTimeout()
. - A call to
clearTimeout()
to restart the delay-counting timer if the underlying routine executes before the chosen debounce delay. - A call to
setTimeout()
to instate the timeout.
The 300ms delay here effectively throws away any quick 'bounce' input
events from being taken into consideration with respect to the final outcome that we ought 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, an input is now any entry of data that doesn't have another 'bounce' within 300ms.
So if you enter two letters very quickly, one after another, the program treats it as one single input.
Ain't that an easy win?
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?
Well, you'll have to leverage a closure to close over a variable holding the timeout's ID that will perform the debouncing, just as we did above, and then also reinstate the timeout within the debounce delay. This delay is also provided to the higher-order function as an argument (in addition to the function to debounce).
Further reading:
To learn more about what exactly is a closure in JavaScript, consider reading JavaScript Functions — Closures from .
Here's the definition of this higher-order function debounce()
:
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(fn, delay, ...args);
}
}
Notice the usage of ...args
rest parameter in the returned function above and ...args
spread argument to setTimeout()
. This makes sure that the signature of the debounced version of the function fn()
is identical to that of fn()
.
To be more strict in the implementation of debounce()
above by calling clearTimeout()
when timerId
is non-null
, and also setting timerId
to null
in the timeout's callback, we have the following:
function debounce(fn, delay) {
let timerId = null;
return function (...args) {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
fn(...args);
timerId = null;
}, delay);
}
}
Practically, it's the same thing happening here, just that we're now more strict in the implementation, clearly initializing timerId
and then resetting it upon the completion of the delay.
With this utility in place, we can rewrite, for example, 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 system, 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 when it isn't necessary and how to use it properly.
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.
Simple!
A good starting point would be 100 - 300 ms. This obviously depends on the event being debounced as well but usually debounce delays tend 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. If you notice it, there are approximately 200 countries in the world. So this input might have its dataset in the form of array on the client.
In this case, it wouldn't make much sense, if at all, to debounce the keydown
event and show suggestions for countries after a delay following an input of data into the field.
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 is also just a matter of iterating over a couple hundred elements.
Without debouncing, the interaction of the autocompleting input field would feel superbly crisp.