DEV Community

Cover image for Debouncing and Throttling
Sanjeev Sharma
Sanjeev Sharma

Posted on • Originally published at frontendcamp.in

Debouncing and Throttling

Another one among the popular frontend interview questions. It tests interviewees knowledge on JS, Performance and FE System Design.

This is question #2 of Frontend Interview Questions series. If you're looking to level up your preparation or stay updated in general, consider signing up on FrontendCamp.


Debouncing and Throttling work on the same principle - delay stuff - but still have very different approach and use cases.

Both the concepts are useful for developing a performant application. Almost all the websites you visit on a daily basis use Debouncing and Throttling in some way or the other.

Debouncing

A well known use case of debouncing is a typeahead(or autocomplete).

Imagine you are building a search feature for an E-commerce website that has thousands of products. When a user tries to search for something, your app would make an API call to fetch all the products that match the user's query string.

const handleKeyDown = async (e) => {
 const { value } = e.target;
 const result = await search(value);
 // set the result to a state and then render on UI
}

<Input onKeyDown={handleKeyDown} />
Enter fullscreen mode Exit fullscreen mode

This approach looks fine but it has some issues:

  1. You're making an API call on each key press event. If a user types 15 characters, that's 15 API calls for a single user. This would never scale.
  2. When the result from these 15 API calls arrives, you only need the last one. Result from previous 14 calls will be discarded. It eats up a lot of user's bandwidth and users on slow network will see a significant delay.
  3. On the UI, these 15 API calls will trigger a re-render. It will make the component laggy.

The solution to these problems is Debouncing.

The basic idea is to wait until the user stops typing. We'll delay the API call.

const debounce = (fn, delay) => {
 let timerId;
 return function(...args) {
  const context = this;

  if (timerId) {
    clearTimeout(timerId);
  };
  timerId = setTimeout(() => fn.call(context, ...args), delay);
 }
}

const handleKeyDown = async (e) => {
 const { value } = e.target;
 const result = await search(value);
 // set the result to a state and then render on UI
}

<Input onKeyDown={debounce(handleKeyDown, 500)} />
Enter fullscreen mode Exit fullscreen mode

We've extended our existing code to make use of debouncing.

The debounce function is generic utility function that takes two arguments:

  1. fn: The function call that is supposed to be delayed.
  2. delay: The delay in milliseconds.

Inside the function, we use setTimeout to delay the actual function(fn) call. If the fn is called again before timer runs out, the timer resets.

With our updated implementation, even if the user types 15 characters we would only make 1 API call(assuming each key press takes less than 500 milliseconds). This solves all the issues we had when we started building this feature.

In a production codebase, you won't have to code your own debounce utility function. Chances are your company already uses a JS utility library like lodash that has these methods.

Throttling

Well, Debouncing is great for performance but there a some scenarios where we don't want to wait for x seconds before being notified of a change.

Imaging you're building a collaborative workspace like Google Docs or Figma. One of the key features is a user should be aware of changes made my other users in real time.

So far we only know of two approaches:

  1. The Noob approach: Any time a user moves their mouse pointer or types something, make an API call. You already know how bad it can get.
  2. The Debouncing approach: It does solve the performance side of things but from a UX perspective it's terrible. Your coworker might write a 300 words paragraph and you only get notified once in the end. Is it still considered real-time?

This is where Throttling comes in. It's right in middle of the two approaches mentioned above. The basic idea is - notify on periodic intervals - not in the end and not on each key press, but periodically.

const throttle = (fn, time) => {
 let lastCalledAt = 0;

 return function(...args) {
  const context = this;
  const now = Date.now();
  const remainingTime = time - (now - lastCalledAt);

  if (remainingTime <= 0) {
   fn.call(context, ...args);
   lastCalledAt = now;
  }
 }
}

const handleKeyDown = async (e) => {
 const { value } = e.target;
 // save it DB and also notify other peers
 await save(value);
}

<Editor onKeyDown={throttle(handleKeyDown, 1000)} />
Enter fullscreen mode Exit fullscreen mode

We've modified our existing code to utilise throttle function. It takes two arguments:

  1. fn: The actual function to throttle.
  2. time: The interval after which the function is allowed to execute.

The implementation is straight-forward. We store the time when the function was last called in lastCalledAt. Next time, when a function call is made, we check if time has passed and only then we execute fn.

We're almost there, but this implementation has a bug. What if last function call with some data is made within the time interval and no call is made after that. With our current implementation, we will lose some data.

To fix this, we'll store the arguments in another variable and initiate a timeout to be called later if no event is received.

const throttle = (fn, time) => {
 let lastCalledAt = 0;
 let lastArgs = null;
 let timeoutId = null;

 return function(...args) {
  const context = this;
  const now = Date.now();
  const remainingTime = time - (now - lastCalledAt);

  if (remainingTime <= 0) {
   // call immediately
   fn.call(context, ...args);
   lastCalledAt = now;
   if (timeoutId) {
     clearTimeout(timeoutId);
     timeoutId = null;
   }
  } else {
    // call later if no event is received
    lastArgs = args;
    if (!timeoutId) {
      timeoutId = setTimeout(() => {
        fn.call(context, ...lastArgs);
        lastCalledAt = Date.now();
        lastArgs = null;
        timeoutId = null;
      }, remainingTime);
    }
  }
 }
}
Enter fullscreen mode Exit fullscreen mode

This updated implementation makes sure we don't miss out on any data.

Lodash also provides a throttle utility function.


Summary

  1. Debouncing and Throttling are performance optimization techniques.
  2. Both of these work on a similar principle - delay things.
  3. Debounce waits for t after the last event is received whereas Throttling executes the fn periodically in t time.
  4. Debouncing is used in search features and Throttling is used in Real-time apps(not limited to these).

Resources

FrontendCamp
lodash

Top comments (7)

Collapse
 
tempmaildetector profile image
TMD

Once had to build a chat like application with read receipts. Scrolling through the messages would essentially send countless messages in one go, so we implemented debouncing just like this. Worked a charm!

Collapse
 
thesanjeevsharma profile image
Sanjeev Sharma

Good one!

Collapse
 
vjnvisakh profile image
Visakh Vijayan

liked the timeout thing in throttling, I encountered this a couple of days back.

Collapse
 
thesanjeevsharma profile image
Sanjeev Sharma

I'm glad you liked it.

Collapse
 
ruithlzz09 profile image
Ruithlzz09

Thanks for share on denounce and throttle usage

Collapse
 
rohil_kpmg profile image
Rohil

Thanks for the explanation. It was precise and crisp

Collapse
 
thesanjeevsharma profile image
Sanjeev Sharma

🫡