Debouncing vs. throttling with vanilla JS
Debouncing and throttling are two related but different techniques for improving performance of events in JavaScript plugins and applications.
The terms are often used interchangeably, but they’re not the same thing. Let’s clear that up.
- With throttling, you run a function immediately, and wait a specified amount of time before running it again. Any additional attempts to run it before that time period is over are ignored.
- With debouncing, you wait a specified amount of time before running a function. The last attempt to run the function is the one that runs, and any previous attempts within the time period are ignored.
Let’s look at examples of both techniques.
Logging button clicks
For both examples, we’ll be counting the number of clicks on a button.
<button id="click-me">Click Me</button>
We’re going to detect when the user clicks the button, and will log it to the console.
var count = 0;
document.addEventListener('click', function (event) {
// Only run if the #click-me button was clicked
if (event.target.id !== 'click-me') return;
// Update the count by 1
count++;
// Log the count to the console
console.log('true count', count);
});
But what if, for performance reasons, you wanted to limit how frequently the log actually happened to once every 3 seconds?
Let’s look at how to do that using throttling and debouncing techniques.
Throttling
With throttling, we’ll setup a variable, wait
, to tell us if we should wait before running the function again or not.
For learning purposes, I also want to show the difference between actual clicks and how many times the throttled function runs. We’ll setup a second variable to track the throttled count, so that we can compare it to the actual number of times the button was clicked.
var count = 0;
var throttleCount = 0;
var wait = false;
When the event listener runs, we’ll still update count
by one.
Then, we’ll update the throttleCount
by one as well, and log both counts into the console.
var count = 0;
var throttleCount = 0;
var wait = false;
document.addEventListener('click', function (event) {
// Only run if the #click-me button was clicked
if (event.target.id !== 'click-me') return;
// Update the count by 1
count++;
// Update the throttleCount
throttleCount++;
// Log the counts
console.log('true count', count);
console.log('throttled count', throttleCount);
});
Now we’re ready to actually throttle the function.
After logging the counts to the console, we’ll set wait
to true
. We’ll use the setTimeout()
method to set it back to false
after 3 seconds (3000 milliseconds)
var count = 0;
var throttleCount = 0;
var wait = false;
document.addEventListener('click', function (event) {
// Only run if the #click-me button was clicked
if (event.target.id !== 'click-me') return;
// Update the count by 1
count++;
// Update the throttleCount
throttleCount++;
// Log the counts
console.log('true count', count);
console.log('throttled count', throttleCount);
// Ignore any future requests for the next 3 seconds
wait = true;
setTimeout(function (event) {
wait = false;
}, 3000);
});
Before updating throttleCount
and logging anything, we’ll check if wait
is true
. If it is, we’ll return
to end the function and do nothing.
var count = 0;
var throttleCount = 0;
var wait = false;
document.addEventListener('click', function (event) {
// Only run if the #click-me button was clicked
if (event.target.id !== 'click-me') return;
// Update the count by 1
count++;
// If currently throttled, ignore the request
if (wait) return;
// Update the throttleCount
throttleCount++;
// Log the counts
console.log('true count', count);
console.log('throttled count', throttleCount);
// Ignore any future requests for the next 3 seconds
wait = true;
setTimeout(function (event) {
wait = false;
}, 3000);
});
Now, the function will only run once every three seconds.
Here’s a demo of throttling. Click the button a bunch of times to see it in action.
Debouncing
Debouncing is actually simpler to implement.
We’ll again create a special variable, debounceCount
, to track the number of debounced clicks versus actual clicks.
var count = 0;
var debounceCount = 0;
With debouncing, we delay running our function until a certain period of time has passed using the setTimeout()
method.
var count = 0;
var debounceCount = 0;
document.addEventListener('click', function (event) {
// Only run if the #click-me button was clicked
if (event.target.id !== 'click-me') return;
// Update the count by 1
count++;
// Update and log the counts after 3 seconds
setTimeout(function () {
// Update the debounceCount
debounceCount++;
// Log the counts to the console
console.log('true count', count);
console.log('debounced count', debounceCount);
}, 3000);
});
However, if the button is clicked again, we want to ignore the currently pending function and replace it with the new one.
To do that, we’ll assign our setTimeout()
to a variable. When the event handler runs, we’ll clear any existing timeout before setting a new one.
var count = 0;
var debounceCount = 0;
var debounce;
document.addEventListener('click', function (event) {
// Only run if the #click-me button was clicked
if (event.target.id !== 'click-me') return;
// Update the count by 1
count++;
// Clear any existing debounce event
clearTimeout(debounce);
// Update and log the counts after 3 seconds
debounce = setTimeout(function () {
// Update the debounceCount
debounceCount++;
// Log the counts to the console
console.log('true count', count);
console.log('debounced count', debounceCount);
}, 3000);
});
As you can see, with debouncing, nothing is logged into the console until three seconds after the last click. If you click the button continuously for six seconds, nothing is logged until three seconds after that, nine seconds from when you started.
Which approach should you use?
Which approach you choose will depend on what you’re trying to do, and what the desired user experience is.
For example, you might use debouncing if you’re sending some updates to a database in reaction to user interactions, since the timing of those updates has little impact on the UI. You might debounce scroll events to run when the user is completely done scrolling.
For interactions that update the UI, throttling might make more sense, because they’ll run at predictable intervals.