Learn Debounce and Throttle Through Animation

Identifying Pain Points

Users interact with websites in many ways, such as clicking buttons, entering text, dragging content, scrolling the page, etc. During the listening process, similar events may be triggered repeatedly, so it is crucial to pay special attention to the user’s input frequency. Excessive triggering of events may lead to:

  • Poor web performance
  • Increased server load

For example, suppose there is an input text box on the screen. Every time a user types something, a request is sent out to fetch third-party resources based on the text content in the box:

input.addEventListener('input', (e) => {
fetch(`/api/getOptions?query=${e.target.value}`);
});

After understanding the pain points, let’s take a look at common solutions — Debouncing and Throttling. These two methods integrate advanced concepts such as asynchronous programming, closure🔗, and rest parameters🔗. Besides being common issues encountered in practice, they are also frequently seen in interviews.

Debounce

Example from Daily Life: Take an automatic door as an example. When a group of people is lined up waiting to enter, it does not close every time someone passes through. Instead, it refreshes the closing time each time someone enters, and when no one else comes in after a period, it automatically closes. This way, it saves a lot of unnecessary “closing and opening actions”.

A more practical example: Referring to the earlier input box search example, when a user is typing, there is no need to rush and submit a request immediately. Instead, a timer can be started, and once the time is up, the latest request can be submitted. If the user types again during the waiting period, the timer resets.

Step 1: Describe the Process

Write a debounce function that has two parameters, the callback function to be called and the waiting time (default is 1000 milliseconds). Create a timeout variable in advance to store the timer. When called:

  1. Stop the currently running timer
  2. Start a new timer
  3. When the timer completes, execute the callback function.

Step 2: Create Function and Closure

Create a debounce function that returns an anonymous function each time it is called, using the feature of closure to allow the timeout variable to be shared among each debounce function.

function debounce(callback, delay = 1000) {
let timeout;
return (...args) => {};
}

Step 3: Implement Timer

Every time the debounce function is called to create a new timer, if there is an existing timer, clear it before setting up the new one, and specify to call the callback function after the defined delay.

function debounce(callback, delay = 250) {
let timeout;
return (...args) => {
if (timerId) {
clearTimeout(timerId);
}
timeout = setTimeout(() => {
callback(...args);
}, delay);
};
}

Throttle

Example from Daily Life: Superheroes have cooldown times when transforming. The transformation function cannot be reused until some time has passed after activation, and any attempts to transform during this time will not succeed.

A more practical example: When the webpage is scrolled to the bottom and new content is desired to be loaded, throttling can be used to limit the number of event triggers.

Step 1: Describe the Process

Write a throttle function with two parameters: the callback function to be called and the waiting time (default 1000 milliseconds). In advance:

  • Create a throttleTimeout variable to store the throttling timer
  • Create a callbackArgs variable to store the parameters for the callback function

When the closure is executed:

  • Record the parameters of the callback function into callbackArgs so that it can be used during the last input
  • If the throttling timer exists, exit the function
  • Execute the callback function (passing the latest parameters)
  • Start the throttling timer with throttleTimeout

When the throttling timer ends:

  • Clear the content of throttleTimeout, indicating no timer is running
  • Call the callback function once more (passing the latest parameters)

Step 2: Create Function and Closure

Create a throttle function that returns an anonymous function each time it is called, using the feature of closure to allow the throttleTimeout and callbackArgs variables to be shared among each throttle function.

function throttle(callback, delay = 1000) {
let throttleTimeout = null;
let callbackArgs = null;
return (...args) => {};
}

Step 3: When the Closure is Executed

callbackArgs = args;

First, store the latest callback parameters so that it can be checked for any pending updates triggered by the callback function when the timer ends. If there are, execute the entire callback function once more.

Next, when the timer does not exist, exit the function directly.

if (throttleTimeout) {
return;
}

Then execute the content of the callback function once.

callback(...callbackArgs);

Since this callback function has been executed, clear the record outside the closure, setting it to null.

callbackArgs = null;

Step 4: Implement Timer

Now to the most crucial part: when the throttling timer finishes, clear its record and check if any unexecuted callback parameters remain. If so, execute the entire callback function once more.

throttleTimeout = setTimeout(() => {
throttleTimeout = null;
if (callbackArgs) {
callback(...callbackArgs);
}
}, delay);

Here is the final result.

function throttle(callback, delay = 1000) {
// Create a closure that allows the following variables to be shared among all throttled events
// Variable 1: Throttle Timer
// Variable 2: Most Recent Event
let throttleTimeout = null;
let callbackArgs = null;
// Return an anonymous function (callback event handler), inputting the parameters of the callback
return (...args) => {
// Store the latest event during each iteration
callbackArgs = args;
// If the throttling timer is running: exit the function
if (throttleTimeout) {
return;
}
// If the throttling timer is not running: execute the callback function and start the throttling timer
// Execute the callback function (using the recorded latest event)
callback(...callbackArgs);
// Clear the recorded latest event
callbackArgs = null;
// Create a new throttling timer, recording in throttleTimeout, to block the input function during activation
// When the timer ends, if there are recorded latest events, execute once more
throttleTimeout = setTimeout(() => {
// When the timer ends, clear throttleTimeout allowing the input function to execute
throttleTimeout = null;
// When the timer ends, check if there are still unexecuted events; if so, execute the entire callback function again
if (callbackArgs) {
callback(...callbackArgs);
}
}, delay);
};
}

Summary

Debouncing consolidates many actions into one; throttling maintains actions at a certain frequency.

You can refer to a practical example from a question I posed in the JS live class at HexSchool in Fall 2022: Learn Debounce and Throttle Through Animation

References