Understanding Throttling and Debouncing

This article was originally written for Bit.

Throttling and debouncing are two widely-used techniques to improve the performance of code that gets executed repeatedly within a period of time.

In this post, we’ll learn how to better use them in order to boost our app’s performance and write better and faster code in JavaScript!

What are they?

To throttle a function means to ensure that the function is called at most once in a specified time period (for instance, once every 10 seconds). This means throttling will prevent a function from running if it has run “recently”. Throttling also ensures a function is run regularly at a fixed rate.

Conversely, a debounced function will ignore all calls to it until the calls have stopped for a specified time period. Only then will it call the original function. For instance, if we specify the time as two seconds, and the debounced function is called 10 times with an interval of one second between each call, the function will not call the original function until two seconds after the last (tenth) call.

Here’s an analogy using a real-world scenario:

Suppose you’re working at your PC and also chatting with your friend Bill on a chat app (such as Telegram), who's telling you a story in bits and pieces (heh). You get push notifications every minute or so. Normally, you’d read every message as it comes in. But you're busy, so you can’t afford to switch context that often. What can you do?

  1. Ignore your notifications, and decide that you’ll check your messages only once every five minutes.
  2. Ignore your notifications. When no new notifications have come in for the last five minutes, assume Bill is done with his story and then check your messages.

The first approach is an example of throttling, while the second is an example of debouncing.

So let’s look at practical reasons for throttling.

Why would you want to throttle or debounce your code?

Supposing you have an event E that invokes a function F when it is triggered. Normally, F would be called every time E is triggered, and that’s okay.

But what happens if E is triggered at a high frequency, for instance, 200 times per second? If F does something trivial like a simple calculation, that might still be okay. However, if F performs an expensive operation like calling an external API, heavy computations or complex DOM manipulations, you’d want to limit how often F gets called to avoid a drop in performance. Another reason why you’d also want to limit how often F gets called is if some other application component depends on the result from F.

Let’s look at two common use cases of throttling and debouncing.

  • Gaming

Image for post

In action games, the user often performs a key action by pushing a button (example: shooting, punching). But, as any gamer knows, users often press the buttons much more than is necessary, probably due to the excitement and intensity of the action. So the user might hit “Punch” 10 times in 5 seconds, but the game character can only throw one punch in one second. In such a situation, it makes sense to throttle the action. In this case, throttling the “Punch” action to one second would ignore the second button press each second.

  • Autocomplete

Often times, search boxes offer dropdowns that provide autocomplete options for the user’s current input. Sometimes the items suggested are fetched from the backend via API (for instance, on Google Maps).

Image for postGoogle Maps with debounced autocomplete

Supposing you’re searching for “Greenwich” and the autocomplete API gets called when the text in the search box changes. Without debounce, an API call would be made for every letter you type, even if you’re typing very fast.

This approach has two major problems:

  1. If the user is a fast typist and types “Green” at a go, the autocomplete box would contain the results for “G”, before switching to those for “Gr”, then “Gre”, and so on. This would be a source of confusion to the user.
  2. API calls aren’t guaranteed to return in the order they were sent. This means the autocomplete request for “Gre” could return after the call for “Green”. This means the user would first see the up-to-date list (items starting with “Green”), which would then be replaced by the out-of-date one (items starting with “Gre”).

So it makes sense to debounce the search here. Debouncing by one second will ensure that the autocomplete function does nothing until one second after the user is done typing.

In summary, throttle says, “Hey, we heard you the first time, but if you want to keep going, no problem. We’ll just ignore you for a while.” Debounce says, “Looks like you’re still busy. No problem, we’ll wait for you to finish!”

Implementing Throttling and Debouncing

Let’s look at how we can implement a simple throttle function in JavaScript. Here’s how it would be used:

// regular call to function handleEvent
element.on('event', handleEvent);
// throttle handleEvent so it gets called only once every 2 seconds (2000 ms)
element.on('event', throttle(handleEvent, 2000));

Let’s note a few things:

  1. Our throttle function will take two parameters: the function to throttle calls to, and the time interval for throttling (in milliseconds).
  2. The throttle function has to return a function. This function is what gets called when the event is triggered. It is this function that keeps track of function calls and decides whether or not to call the original function.
  3. To keep track of when last the function was called, we’ll make use of the fact that JavaScript functions are regular objects that can have properties. We can have a lastCall property on our function that records the last time the function was called.
  4. How do we determine whether or not to call the original function? Two scenarios: if this is the first ever call, or if the throttle time has elapsed since the function was called last.

Putting it all together, here’s what we come up with (and a usage example):

function throttle(f, t) {
  return function (args) {
    let previousCall = this.lastCall;
    this.lastCall = Date.now();
    if (previousCall === undefined // function is being called for the first time
        || (this.lastCall - previousCall) > t) { // throttle time has elapsed
      f(args);
    }
  }
}

let logger = (args) => console.log(`My args are ${args}`);
// throttle: call the logger at most once every two seconds
let throttledLogger = throttle(logger, 2000); 

throttledLogger([1, 2, 3]);
throttledLogger([1, 2, 3]);
throttledLogger([1, 2, 3]);
throttledLogger([1, 2, 3]);
throttledLogger([1, 2, 3]);

// "My args are 1, 2, 3" - logged only once

Next, a basic debounce function, which will be used like this:

// regular call to function handleEvent
element.on('event', handleEvent);
// debounce handleEvent so it gets called after calls have stopped for 2 seconds (2000 ms)
element.on('event', debounce(handleEvent, 2000));

Here are the considerations we need to take into account:

  1. Debouncing implies waiting. We have to wait for calls to the function to stop for t milliseconds. We’ll make use of setTimeout to implement wait functionality.
  2. Since we can’t just tell our function to stick around until calls stop, we’ll use setTimeout to get around this. Whenever the function is called, we’ll schedule a call to the original function if t elapses without any more calls. If another call happens before t elapses, we’ll cancel the previously scheduled call and reschedule.
  3. Once again, we’ll use the function properties to track calls. In addition to storing lastCall, we’ll store a property called lastCallTimer. This will hold the value returned by setTimeout, so we can cancel the timer (using clearTimeout) if necessary.

Here’s the end result:

function debounce(f, t) {
  return function (args) {
    let previousCall = this.lastCall;
    this.lastCall = Date.now();
    if (previousCall && ((this.lastCall - previousCall) <= t)) {
      clearTimeout(this.lastCallTimer);
    }
    this.lastCallTimer = setTimeout(() => f(args), t);
  }
}

let logger = (args) => console.log(`My args are ${args}`);
 // debounce: call the logger when two seconds have elapsed since the last call
let debouncedLogger = debounce(logger, 2000);

debouncedLogger([1, 2, 3]);
debouncedLogger([4, 5, 6]);
debouncedLogger([7, 8, 9]);

// "My args are 7, 8, 9" - logged after two seconds

The lodash library also provides more robustthrottle and debounce functionality, allowing you to do:

element.on(scroll, _.throttle(handleScroll, 100));
$(window).on(resize, _.debounce(function() {
}, 100));

If you don’t want to import the entire lodash library, you can import the submodules instead:

const debounce = require('lodah/debounce');
const throttle = require('lodash/throttle');
// and use like before:
element.on(scroll, throttle(handleScroll, 100));
$(window).on(resize, debounce(function() {
}, 100));

Conclusion

By far, the most useful applications of throttling and debouncing are on the frontend, where users can perform actions at rates we cannot control. They can also be useful to the server, though. API servers often implement throttling (“rate limiting”) to prevent the application from being overloaded.

Throttling is most useful when the input to the function call doesn’t matter, or is the same each time (for instance, a scroll event), whereas debouncing fits best when the result of the most recent event occurrence (for instance, when resizing a window) is what matters to the end user. Thanks for reading and please feel free to comment and ask/add anything! Cheers 👏



I write about my software engineering learnings and experiments. Stay updated with Tentacle: tntcl.app/blog.shalvah.me.

Powered By Swish