Back

Explain Debouncing and Throttling

🌿 Intermediate
JS
6m

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} />;

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)} />;

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)} />;

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); } } }; };

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

lodash