Debouncing vs. Throttling: Which Technique to Use in JavaScript?

In this blog, we are going to learn about Debouncing and throttling when we need to use them and how they are different from each other and at last when to use what.
But before going that deep first let's learn why even we need that and let's learn it by example directly.

// SIMPLE FETCH CALL TO GET PRODUCT LIST
const input = document.querySelector('#search-input');
input.addEventListener('input', (event) => {
  const query = event.target.value;
  fetch(`/api/products?search=${query}`)
    .then((res) => res.json())
    .then((data) => {
      // Do whatever you love to do with data
    });
});

In the above code snippet, we are fetching the product list and the important thing to note here is that this fetch request will be called whenever the input value gets changed. Suppose the user tries to search for Samsung now each when user will press S then this fetch request will be triggered and it will return all the data that have S in it then the user will press a so now again this fetch API call will be triggered and it will return all the results will which has Sa in it and this way this API will be triggered each time when the user is pressing any key so for Samsung it will be called 8 times. So here is the question can we do something better can we reduce the number of times this fetch() API gets called the answer is yes and this is where the debouncing and throttling come into the picture.

Debouncing: Let's talk about debouncing first because when it comes to scenarios like autocomplete text debouncing is the ideal scenario that we are going to use.
In debouncing we are delaying our function call by a set period of time. If nothing happens during that time then the function will run just like normal, but if something happens that causes the function to be called again during the delay then the delay will be restarted.

Let's take the example where the fetch function will be called only when the user has stopped typing for 1 second given the example if the user pressed Sa and then take a pause of 1 second before typing the next word then the fetch function will be called only for Sa thing about the scenario when a user types entire word Samsung without taking any pause, in that case, the fetch function will be called once only where previously it was getting called 7 times for word Samsung. Let's take a look at how to implement debouncing.

function debounce(fn,delay){
 let timeout;
 return function (...args){
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    fn(...args)
   },delay); // ... is spread operator 
 }
}

Here in the above example, debouncing will take function fn as the first parameter and delay as the second parameter. Here the debounce function returns a new function that delays calling the original function until a specified amount of time has passed since the last call and this helps to prevent the original function from being called too frequently, which can improve performance. The function also clears any existing timeout when called again before the delay has passed and at this point, we need to understand how it clears the existing timeout when called again before the delay has passed because this is where its magic is happening so request you to run this code at least once to understand how it works. Now let's look at how we implement this.

const searchProducts = debounce(query => {
  fetch(`/api/products?search=${query}`)
    .then(res => res.json())
    .then(data => {
       console.log('DO WHATEVER YOU LOVE TO DO WITH THIS DATA')
     })
}, 1000)

input.addEventListener("input", e => {
  searchProducts(e.target.value)
)}

Debouncing is perfect for autocomplete but is also useful anywhere you want to group multiple triggers into one trigger such as limiting infinite loading to not overwhelm your servers.

Throttling: Now throttling is used for the same reason as how can we reduce the number of time-specific functions getting called an only difference is that both debouncing and throttling are used in different use cases.

See in debouncing function get executed only if the user passes the specific delay time or in other words when the user types something if the difference between two typed char is more than delayed then it will execute.

So if we have passed the 300 ms delay and the user type word Samsung and between each char, it takes a pause of 200 ms then only the fetch() function will be executed only once.

Now in the case of the throttling function gets executed after every certain delay that we passed example if we pass a delay of 1000 ms and then with throttling it will be executed immediately when it is called and then at most once per second while the user is actively typing. Let's take a look at an implementation to understand what is going on.

function throttle(cb, delay = 250) {
  let shouldWait = false

  return (...args) => {
    if (shouldWait) return
    cb(...args); // See first time we are seting false so it will be called immediately
    shouldWait = true
    setTimeout(() => {
      shouldWait = false
    }, delay)
  }
}

Debounce and throttle both take the same arguments, but the main difference you can see in throttle vs debounce is that our callback is invoked immediately and not inside the timeout. The only thing the timeout does is set the shouldWait variable to false. When we first call toggle it will run our callback and set shouldWait to true. If the throttle is called again during the delay it will do nothing and thats because of the if check we have at the start. Once the delay is over shouldWait will be set to false which means if the throttle is called again it will run.

This means if our user types one character every 300 milliseconds and our delay is 1 second our throttle function will work like this. Now if we run above you will see that the way it's implemented will skip some arguments at last and won't run to those this may be fine for some of the solutions but we can fix it by taking one extra argument which is waiting for arms. Let's see the example.

function throttle(cb, delay = 1000) {
  let shouldWait = false
  let waitingArgs
  const timeoutFunc = () => {
    if (waitingArgs == null) {
      shouldWait = false
    } else {
      cb(...waitingArgs)
      waitingArgs = null
      setTimeout(timeoutFunc, delay)
    }
  }
  return (...args) => {
    if (shouldWait) {
      waitingArgs = args
      return
    }
    cb(...args)
    shouldWait = true
    setTimeout(timeoutFunc, delay)
  }
}

Now autocomplete text boxes are not the best use case for throttling, but when you are dealing with things like resizing elements, drag and drop, scrolling, or other events that occur many times and you want to get updated on their values periodically then the throttle is ideal. The reason throttle is ideal for these scenarios is that every time the delay ends you will get updated information on the event while debounce needs to wait for a delay between inputs to trigger. Essentially, the throttle is ideal when you want to group multiple events into one event periodically.

If we think about this in the example of resizing an element with a delay of 250 milliseconds, the throttle will get the element's size every 250 milliseconds while debounce will only get the element's size when it has been 250 milliseconds since the resize has finished.

In Conclusion, Using debounce and throttle techniques can greatly improve the performance of your website or application, especially when dealing with events that trigger functions multiple times in quick succession. By grouping these events, you can reduce the number of requests sent to the server, saving you money on server costs, and also minimize the amount of data transferred to the user, saving them money on data costs. Ultimately, these optimizations lead to a better user experience and a more performant application. Thanks for reading hope you enjoyed it :).