Throttle and Debounce in Javascript and React
source link: https://codefrontend.com/debounce-throttle-js-react/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Both throttle and debounce are used to optimize expensive, frequently executed actions. They're two of the most common performance optimization techniques.
I'll show you how to implement them yourself and how to use them in JavaScript and React.
What is debouncing?
Picture this: you want a search input where results are queried automatically as you're typing into it.
You wouldn't want to ping the backend on every keystroke, rather you only care about the final value. That's the perfect use case for a debounce function, in fact it's a very common one.
Implementing a basic debounce function
Debounce is implemented using a timer, where the action executes after the timeout. If the action is repeated before the timer has finished, we restart the timer and queue the latest action.
Here's the most basic implementation in TypeScript:
function debounce<Args extends unknown[]>(fn: (...args: Args) => void, delay: number) {
let timeoutID: number | undefined;
const debounced = (...args: Args) => {
clearTimeout(timeoutID);
timeoutID = window.setTimeout(() => fn(...args), delay);
};
return debounced;
}
Simple debounce function.
- Create a function that accepts a function to debounce and the timeout delay as arguments.
- The debounce function returns a new function. When it's executed, it creates a timer to execute the original function after the delay and cancels the previous timer.
Here's how to use it:
const expensiveCalculation = debounce(() => {
// 🚩 Do the expensive calculation
}, 1000)
// 👇 Frequently called function
function onChange() {
// 👇 Will run at most once per second
expensiveCalculation()
}
Example debounce usage.
Flushing the debounce result
What if we sometimes need to call the action before the delay and cancel any pending executions? We call that flushing.
We can attach an extra method to the original debounce function implementation, that runs the pending action instantly and clears the timer:
function debounce<Args extends unknown[]>(fn: (...args: Args) => void, delay: number) {
let timeoutID: number | undefined;
let lastArgs: Args | undefined;
const run = () => {
if (lastArgs) {
fn(...lastArgs);
lastArgs = undefined;
}
};
const debounced = (...args: Args) => {
clearTimeout(timeoutID);
lastArgs = args;
timeoutID = window.setTimeout(run, delay);
};
debounced.flush = () => {
clearTimeout(timeoutID);
run();
};
return debounced;
}
Debounce function with flush support.
- Store the arguments of the last action into an array when calling the debounced function.
- Create a new
debounce.flush
function that runs the action with the most recently used arguments and clears the timer and cached arguments.
Call debounce.flush()
to run the action immediately:
const expensiveCalculation = debounce(() => {
// Expensive calculation...
}, 1000)
function onChange() {
expensiveCalculation()
}
function onClose() {
// 👇 Instantly runs the calculation and cancels any pending calls
expensiveCalculation.flush()
}
Flushing the debounce function.
What is throttling?
The most common use case from my experience is to optimize the resize and scroll handlers. That's especially important in React because they often trigger state updates that are responsible for triggering re-renders of your components.
The solution to this problem is to call the handlers intermittently. Most of the time we don't need to keep 100% in sync with the resize or scroll events, so we can call throttle the handler functions. I like to think of them as lossy event handlers.
Implementing a throttle function
The throttle function is implemented using a timer that puts the throttled function on cooldown:
- Create a throttle function that accepts a callback and the cooldown duration arguments.
- The throttle function returns a new function, which when executed, stores the call arguments and starts the cooldown timer.
- When the timer finishes, execute the action with the cached arguments and clear them.
function throttle<Args extends unknown[]>(fn: (...args: Args) => void, cooldown: number) {
let lastArgs: Args | undefined;
const run = () => {
if (lastArgs) {
fn(...lastArgs);
lastArgs = undefined;
}
};
const throttled = (...args: Args) => {
const isOnCooldown = !!lastArgs;
lastArgs = args;
if (isOnCooldown) {
return;
}
window.setTimeout(run, cooldown);
};
return throttled;
}
The throttle function code snippet.
Here's how you would use it:
const expensiveCalculation = throttle(() => {
// 🚩 Do the expensive calculation
}, 100)
function onResize() {
// 👇 Will be called once every 100ms
expensiveCalculation()
}
Example throttle usage.
Using throttle and debounce in React
In React, new functions are created every time the component re-renders, which is not great for our debounce/throttle implementation which relies on the closure staying the same.
When you use debounce and throttle in React, make sure to wrap them with useMemo
hook:
const handleChangeText = useMemo(() =>
debounce((e: ChangeEvent<HTMLInputElement>) => {
// Handle the onChange event
}, 1000),
[]);
const handleWindowResize = useMemo(() =>
throttle(() => {
// Handle the onResize event
}, 100),
[]);
Wrapping the debounce and throttle with useMemo
.
Custom useDebounce and useThrottle hooks
You can also turn this into custom react hooks:
import { DependencyList, useMemo } from 'react';
import debounce from './debounce';
import throttle from './throttle';
function useDebounce<Args extends unknown[]>(
cb: (...args: Args) => void,
delay: number,
deps: DependencyList,
) {
return useMemo(() => debounce(cb, delay), deps);
}
function useThrottle<Args extends unknown[]>(
cb: (...args: Args) => void,
cooldown: number,
deps: DependencyList,
) {
return useMemo(() => throttle(cb, cooldown), deps);
}
Code snippet for useDebounce
and useThrottle
hooks.
Debounce example in React
Here's how you could use the custom useDebounce
hook in React:
import { ChangeEvent, useState } from 'react';
import useDebounce from './useDebounce';
function DebounceWithFlushExample() {
const [text, setText] = useState('');
const handleChangeText = useDebounce(
(e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
},
5000,
[],
);
return (
<div>
<div>Text (5 second): {text}</div>
<input type="text" onChange={handleChangeText} />
<button onClick={handleChangeText.flush}>Flush</button>
</div>
);
}
Example of using debounce in React.
Throttle example in React
Here's how you could use the custom useThrottle
hook in React:
import { useEffect, useState } from 'react';
import useThrottle from './useThrottle';
type Range = 'small' | 'medium' | 'large';
const sizeToRange = (size: number): Range => {
if (size < 600) {
return 'small';
} else if (size > 1200) {
return 'large';
}
return 'medium';
};
function ThrottleExample() {
const [range, setRange] = useState(sizeToRange(window.innerWidth));
const handleWindowResize = useThrottle(
() => {
// Execute some expensive operation
setRange(sizeToRange(window.innerWidth));
},
100,
[],
);
useEffect(() => {
window.addEventListener('resize', handleWindowResize);
return () => {
window.removeEventListener('resize', handleWindowResize);
};
}, [handleWindowResize]);
return <div>Screen size (resize to see): {range}</div>;
}
export default ThrottleExample;
Example of using throttle in React.
Here are both examples in CodeSandbox so you can play around with them:
When to use throttling and when to debounce?
Use debounce when you don't care about the intermediate results because the action only makes sense with the last result. An example of that is - search results.
Use throttle when you need intermediate results, but a small delay is acceptable. For example, resizing or scrolling.
As always, you can find the code examples in my GitHub repository. Next, learn to upload files with react:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK