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:
- Stop the currently running timer
- Start a new timer
- 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
- StackOverflow - Difference Between throttling and debouncing a function
- nissen.tech - 為什麼我們需要閉包(Closure)?它是冷知識還是真有用途?
- Web Dev Simplified - How To Implement Debounce And Throttle In JavaScript
- Skilled.dev - Interview Question Debounce
- Skilled.dev - Interview Question Throttle
- redd.one - Debounce vs Throttle: Definitive Visual Guide