Debounce and throttle solve the same underlying problem — a function is firing far more often than it needs to — but they solve it in opposite ways. Debounce silences a function until the events stop, then runs it once. Throttle lets the function run on a steady schedule the whole time the events keep firing. Mixing them up does not just misuse a utility, it produces a UI that feels wrong: a debounced scroll indicator that freezes until you stop scrolling, or a throttled search box that fires a request on every half-typed word.
What changed in 2026
- Framework-level batching reduced some of the need for debounce on renders. React and similar frameworks now batch state updates more aggressively, but that only affects re-render frequency, not how often you call an external API — you still need debounce or throttle for that.
- AbortController is the default pairing with debounce for network calls. Canceling the in-flight request from the previous keystroke, not just delaying the new one, is now standard for search-as-you-type.
- requestAnimationFrame-based throttling became the norm for scroll and pointer handlers, keeping throttled visual updates in sync with the browser paint cycle instead of an arbitrary millisecond interval.
- DevTools performance panels now flag excessive handler invocations directly, making an un-throttled scroll or resize listener easy to spot before it ships.
How debounce works
Debounce resets a timer on every call. Only when the calls stop for the full delay does the function actually run.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const onSearch = debounce((query) => fetchResults(query), 300);
input.addEventListener("input", (e) => onSearch(e.target.value));
Every keystroke cancels the previous timer. The search only fires once typing pauses for 300 milliseconds.
How throttle works
Throttle tracks the last time the function ran and blocks calls until the interval has passed, regardless of how many events arrive in between.
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn(...args);
}
};
}
const onScroll = throttle(() => updateProgressBar(), 100);
window.addEventListener("scroll", onScroll);
The progress bar updates roughly every 100 milliseconds while scrolling continues, instead of on every single scroll event.
Which one fits which event
| Event or scenario |
Debounce |
Throttle |
Why |
| Search-as-you-type |
Yes |
No |
Wait for the user to pause before firing one request |
| Window resize (recalculating layout) |
Rarely |
Yes |
Needs steady updates while resizing, not only at the end |
| Infinite scroll loading |
No |
Yes |
Must check scroll position repeatedly at a steady pace |
| Form field validation |
Yes |
No |
Validate once the user stops typing, not every keystroke |
| Double-click submit prevention |
Yes |
Sometimes |
Block repeat submissions until the user pauses |
| Drag-to-resize or mouse tracking |
No |
Yes |
Needs regular updates during continuous motion |
| Auto-save draft |
Yes |
Sometimes |
Save after a pause, not on every character typed |
Common mistakes
Picking the same delay for both without thinking about the event's rhythm. A 300ms debounce feels natural for typing; a 300ms throttle can feel sluggish for a progress indicator that should track the pointer closely.
Debouncing something that needs to feel continuous. A live progress bar or drag preview debounced instead of throttled looks frozen until the interaction stops, which reads as a bug even though the code works as written.
Not canceling a pending debounced call on unmount. An unmounted component that still fires a debounced state update after cleanup throws a warning or leaks memory. Clear the timer in the effect cleanup function.
Re-creating the debounced or throttled function on every render. If the wrapped function is redefined each render instead of memoized, its internal timer resets constantly. See what the virtual DOM is for why unnecessary re-renders are worth guarding against — a broken debounce is often the cause.
FAQ
Is throttle just debounce with a maximum wait time added?
They can be built to look similar with a maxWait option, but the core difference remains: debounce measures silence between events, and throttle measures a fixed clock interval regardless of spacing.
Should I debounce or throttle a window resize handler?
Throttle it if the layout needs to update smoothly as the window changes size. Debounce it only if a frozen layout until the resize stops is acceptable.
Do React hooks change any of this?
No. You still need a stable debounced or throttled function, usually built with useMemo or useRef, or a re-render will recreate it and reset the timer.
What delay should I actually use?
There is no universal number. Start around 200 to 300 milliseconds for typing-related debounce and 100 to 200 milliseconds for scroll or resize throttling, then adjust to how the interaction feels.
Where to go next