Debouncing and Throttling

Debouncing and Throttling

Debouncing and Throttling are techniques that are used to limit the execution of a function. While building a web application we have to be very performant, but we might write some functions which are time-consuming and invoking them frequently will regress our browser performance as JavaScript is a single-threaded language. In such cases to optimize performance, we use the above-mentioned techniques to limit such time-taking function calls.

We will be talking more about these techniques with examples to get a better insight into them. So let's get started.

In this article I will use a simple search bar as an example for debouncing and four divs of separate colors each having 500px height as an example for throttling

Debouncing

Debouncing is the technique where instead of running a function every single time based on the given condition(here our condition is with change in input) the method waits for a predetermined period of time before executing. When the same function is invoked again before the predetermined delay elapses, the previous process is canceled and the timer is reset.

Default Case

// index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="script.js" defer></script>
</head>
<body>
    <input type="text" id="search">
</body>
</html>
//script.js

let counter = 0;
const input = document.getElementById("search");

input.addEventListener("input", (e)=>{
    counter++;
    console.log("Number of times event triggered : " + counter);
});

In the above video, we can see that with every change in input our event listener is running and we log the number of times we trigger our event on the console. Now this works absolutely fine and is super fast when on our local device and there are no performance issues no matter how poor our network connection is.

But suppose we are working on a real world application and instead of just console logging we are fetching some data from an API which will take some time as it is asynchronous. Now, calling the API with every change in input is going to make a lot of network requests to the server which will eventually slow down our application and if someone with poor connection would be using our application it will burn through their data at an insane rate.

So our application will just create a huge MESS!!! which is not at all desired. So let's see how to work around this.

Optimized Case

First, we will see the code and then I will walk you through it

// script.js

let counter = 0;
const input = document.getElementById("search");

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

input.addEventListener("input", debounce((e)=>{
    counter++;
    console.log("Number of times event triggered : " + counter);
}, 1000));

In the above code sample, we can see the debounce function takes in 2 arguments, a callback function and a duration in milliseconds and it also returns a function.
Now, let's break the function to get a better hold of it

In the first line, we initialize a variable timer. This line is executed once. One thing to note here is that we call our wrapped function many times, but we call debounce() only at the start.

Now whenever the wrapped function is triggered we can see two things happening

  • clearTimeout(timer) -> We cancel any pre-existing timeout.
  • timer = setTimeout(() => { callback.apply(this,args) }, delay) -> We schedule a new timeout, based on the amount of time indicated by the delay argument. When the timeout expires, we call our callback function with apply, and feed it whatever arguments we have.

So it WORKS!!! We have managed to reduce the triggering of our event listener.
Let's do a bit of deep-diving into what is actually happening.

The very first time the user types in an input, nothing gets scheduled yet! The clearTimeout does nothing as there is no timer currently.

setTimeout returns a number, a reference to the specific timeout in question. We store that value in our timer variable as it is outside of our wrapped function's scope and hence it will persist.

Now if the user hasn't finished typing, the wrapped function gets called repeatedly and as timer currently points to a scheduled timeout, the first line clearTimeout cancels it and reschedules a new one.

If the user keeps on typing, this cycle will keep repeating until they stop typing and the moment the mentioned delay time elapses, our timeout will fire back, and the code will ultimately run.

Now all this might look really complex but this technique is very efficient and is also used in real world projects as scheduling and cleaning up timeouts is a very quick, low-memory operation.

Let's move to the next one...

Throttling

Throttling is used for a similar purpose which is to rate limit function calls but it works in a different way. Throttling is the technique where instead of running a function every single time based on the given condition(here our condition is with every scroll) the method runs only once in a specified time period. It ensures that a function is run regularly at a fixed rate.

Default Case

// index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="script.js" defer></script>
</head>
<body>
    <div style="background-color: red; height: 500px;"></div>
    <div style="background-color: green; height: 500px;"></div>
    <div style="background-color: blue; height: 500px;"></div>
    <div style="background-color: yellow; height: 500px;"></div>
</body>
</html>
//script.js

let counter = 0;
const input = document.getElementById("search");

window.addEventListener("scroll", (e)=>{
    counter++;
    console.log("Number of times event triggered : " + counter);
});

In the above video, we can see that every time we scroll through our div our event listener is running and we log the number of times we trigger our event on the console. Now again as we are on our local machine, this works absolutely fine without any performance issues no matter how poor our network connection is.

But suppose we are scrolling through a content-loading webpage in a real world application and if the event is fired such frequently it will regress the browser performance or may take a negative impact on the UI which is not good for the user experience. Or assume, we are playing a game and due to excitement we often press a performing key(eg : shooting, punching) way too many times but the game only allows us to do that move suppose only once in a second. So in this scenario also we need to limit the user to throw only one punch in a second no matter how many times they press the key.

So let's see how to handle such situations.

Optimized Case

First, we will see the code and then I will walk you through it

// script.js

let counter = 0;

const throttle = (callback, timer)=>{
    let flag=false;
    return (...args)=>{
        if(flag){
            return;
        }
        callback.apply(this,args);
        flag=true;
        setTimeout(() => {
            flag=false;
        }, timer);
    }
}

window.addEventListener("scroll", throttle((e)=>{
    counter++;
    console.log("Number of times event triggered : " + counter);
}, 1000));

In the above code sample, we can see the throttle function takes in 2 arguments, a callback function and a timer in milliseconds and it also returns a function.
Now, let's break the function to get a better hold of it

In the first line we initialize a variable flag, and set it to false. This line is executed once along with our throttle function but we call our wrapped function several times. Now whenever the wrapped function is triggered we can see the following things happening

  • if(flag){ return } -> Function will return without scheduling any timeout if flag is true.
  • callback.apply(this, args) -> If flag is false we proceed and call our callback function with apply, and feed it whatever arguments we have.
  • flag=true -> We then set flag to true.
  • setTimeout(() => { flag=false }, timer) -> We schedule a new timeout, for the amount of time indicated by timer. When the timeout expires, we set flag to false so that we can again proceed with calling our callback to trigger the event.

So it WORKS!!! We have managed to massively reduce the number of times we call our function. Now again time to do a bit of deep-diving into what is happening.

The very first time the user scrolls, flag is set to false and hence the callback function is called and our event is triggered initially. It is then followed by setting flag to true and scheduling a timeout.

setTimeout will execute after the pre-determined timer elapses and will finally set the flag to false.

Now, if the user scrolls before the elapsing of that timer, flag will still be true and thus our wrapped function will just return instead of calling our callback and we will not be able to schedule a new timeout. This ensures that the callback function will only be called once in a pre-determined interval.

Again all this might look really complex but this technique is very efficient and is also used in real world applications as scheduling timeouts is very quick as well as a low-memory operation.

Conclusion

Debounce and Throttle both improve the performance of web applications. However, they have different use cases. Debounce is usually used when we care about the final state whereas Throttle is best for situations when we want to handle all the intermediate states but at a controlled rate. In this article, we have discussed both the topics with a logical approach and code implementation.

Debounce and Throttle along with their code are important topics for interviews

I hope you have enjoyed reading this article and if it helped you in any way do share and leave your feedback.
Thank you for reading and good luck optimizing the performances of your scripts using the above-discussed techniques

Did you find this article valuable?

Support Deepayan Mukherjee by becoming a sponsor. Any amount is appreciated!