Asyncintermediate
Debouncing and Throttling
Two techniques for limiting how often a function runs in response to rapidly repeating events, like scrolling or typing.
Both limit how often a function runs during rapid events. They differ in which calls they keep.
Debouncing is an elevator door that keeps resetting its close timer as people approach. Throttling is a metered highway on-ramp letting one car through every few seconds no matter the queue.
Key Concepts
1
debounce wait until the events stop, then run once
throttle run at most once per interval, while events continue2
Debounce: search-as-you-type.
function debounce(fn, ms) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
}3
input.addEventListener('input', debounce(search, 300));4
Every keystroke cancels the pending call and schedules a new one. Typing "javascript" fires one search, 300
ms after the last letter — not ten.
5
Throttle: scroll position.
function throttle(fn, ms) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= ms) {
last = now;
fn(...args);
}
};
}6
window.addEventListener('scroll', throttle(updateHeader, 100));7
Scrolling fires hundreds of events a second. Throttling runs the handler ten times a second, evenly, while
scrolling continues.
8
Which to use is decided by one question: do you need updates during the activity?
search input no -> debounce; only the final query matters
window resize no -> debounce; act when resizing stops
scroll position yes -> throttle; the header must move as
you scroll
button double-click yes -> throttle; act on the first, ignore
the rest
autosave a draft no -> debounce9
The leading-edge variant. Plain debounce waits before the first call, which feels unresponsive on a
button. A leading-edge debounce runs immediately and then ignores further calls until things settle:
10
submitButton.addEventListener('click', debounce(submit, 1000, { leading: true }));11
The mistake that makes both do nothing.
// wrong -- a new debounced function on every render
<input onChange={debounce(search, 300)} />12
Each render creates a fresh timer, so nothing is ever cancelled. The debounced function must be created
once — useMemo, useCallback, or outside the component.
useMemouseCallback
13
And cancellation. A pending debounced call after a component unmounts still fires. Real
implementations expose .cancel(), and calling it on teardown is what prevents the update-after-unmount
warning.
.cancel()
14
What the interviewer is probing.1. "What is the difference?" Probing: the two behaviours. Stalls: "Both delay things."
Moves up: debounce waits for a quiet period and runs once at the end; throttle runs at most once
per interval while events continue.
15
2. "Which suits a search box, and which a scroll handler?" Probing: applying it. Stalls:
"Either." Moves up: debounce for search, so one request fires when typing stops; throttle for
scroll, so updates keep happening at a bounded rate.
16
3. "What breaks if you create the debounced function inside a render?" Probing: the identity
problem. Stalls: "Nothing." Moves up: a new function each render means the timer is never
shared, so it never debounces — it must be created once.
17
4. "What is the leading edge option for?" Probing: responsiveness. Stalls: "Nothing." *Moves
up:* running immediately on the first event and then suppressing, so the UI feels instant while
still being limited.