Engineering

Debounce vs Throttle in JavaScript

Introduction

If you are a software developer, you probably know that one of the most important things in developing a new product is performance. You are doing your best to make sure your app loads fast, each functionality is clear and simple to use and there are no bugs. You want the user to quickly and easily interact with it. It's clear that the fewer functions and request are called, the faster your app is.

In this article, I will shortly describe two techniques you might use to reduce the number of your function calls caused by a user interacting with your page. Using them might make you calling heavy calculation functions or API requests less often. Let's dive into debounce and throttle!

Debounce

function debounce<T extends (...args: any[]) => any>(
  func: T,
  delay: number
) {
  let debouncedTimeoutId: ReturnType<typeof setTimeout>;

  return function (...args: Parameters<T>) {
    clearTimeout(debouncedTimeoutId);
    debouncedTimeoutId = setTimeout(func.bind(null, ...args), delay);
  };
}

Gist link

Let your user stop interacting with the interface and then call the function - this is how I would describe debouncing a function in a single sentence.

A practical use case of debounce is waiting until the user stops typing in input. This way you are not disturbing the user in typing and for example not displaying results the user doesn't want to see (If he wanted to see it, he would stop typing). It will reduce a number of unnecessary, heavy calculations or API requests.

This is only one of many examples you can use debounce in - basically you will use it in event handlers that run frequently (keyboard and mouse events, window resizing, etc.).

Let's check what's happening in the code.

We declare a debounce function which takes two arguments:

  • func - a function to debounce
  • delay - time (in ms) after the function can be called

We declare a debouncedTimeoutId variable, with initial value of undefined, which is "closed" in debounce function (this way we are making sure it cannot be accessed from the outside - more about closure). We will assign a value returned from setTimeout (id number) to it.

We return an anonymous function which will call our callback with the same arguments. Returned function clears the timeout with id of debouncedTimeoutId and sets another timeout storing its id in the same variable - this way our callback will run only if we stop calling debounced function for a given amount of time (delay). It won't be executed while we are spamming, because each time the event is fired, the timeout gets cleared.

You can use the function this way:

window.addEventListener('resize', debounce(() => console.log('debounced!'), 1000));

Throttle

function throttle<T extends any[]>(
  func: (...args: T) => void,
  delay: number
) {
  let throttleTimeoutId: ReturnType<typeof setTimeout> | undefined;

  return function (...args: T) {
    if (throttleTimeoutId) return;

    throttleTimeoutId = setTimeout(() => {
      func(...args);
      throttleTimeoutId = undefined;
    }, delay);
  };
}

Gist link

Let's start with single sentence again: While the user is interacting with the interface, call the function once in a given period of time.

Throttle is about blocking a function from running for a specified time - some time has to elapse before it can be executed again. So, when the user is typing for a while, the function will run only if it's not blocked.

With throttle, you can listen for continuous events (like scrolling or window resizing) and run your function less frequently.

This one might seem a bit more complicated, so let's dive into the code again:

We declare a throttle function which takes two arguments:

  • func - a function to throttle
  • delay - time (in ms) after the function will be called

We declare a throttleTimeoutId variable.

We return a function which will call our callback with the same arguments. But before actually running a function, we are checking whether it's already waiting to be called. If it's waiting, do nothing and let it execute first before being called again. After given time (delay) run the function again.

Use it:

window.addEventListener('resize', throttle(() => console.log('throttled!'), 1000));

Wrap it up!

I wanted to keep this article as short and simple as possible (I think that playing around with demo and reading implementation code is enough to understand these two techniques). Keep in mind that it's my implementation and you might find better code and explanation. I'm glad if it helped you understand/implement throttle or debounce. Thank you for reading this article, keep learning!

Play more with the demo: https://debounce-vs-throttle.vercel.app