Throttle and Debounce
Two techniques for limiting how often expensive work runs in response to a high-frequency event — throttle caps the rate, debounce waits for a pause.
throttle(fn, intervalMs) / debounce(fn, delayMs)
Purpose
A high-frequency event (scroll, resize, input) can fire far more often than the resulting
work actually needs to run. Throttle and debounce both limit how often a function runs, but for
different intents.
Throttle
function throttle(fn, intervalMs) {
let lastRun = 0;
return function (...args) {
const now = Date.now();
if (now - lastRun >= intervalMs) {
lastRun = now;
fn(...args);
}
};
}
window.addEventListener('scroll', throttle(updateScrollIndicator, 100));
Runs at most once per intervalMs, regardless of how often it’s triggered — suited to “keep this
roughly continuously updated, but not on every single event.”
Debounce
function debounce(fn, delayMs) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delayMs);
};
}
searchInput.addEventListener('input', debounce(runSearch, 300));
Waits for a pause of delayMs with no new trigger before running once — suited to “only run once
the user has stopped” (search-as-you-type, resize-triggered layout recalculation).
Behavior notes
- Throttle guarantees periodic execution during continuous triggering; debounce can indefinitely postpone execution if triggering never pauses long enough.
- Some debounce implementations offer a “leading edge” option (run immediately on the first trigger, then debounce subsequent ones) for cases where an instant first response matters.
- Both are typically implemented as closures — the “last run” or “pending timeout” state is private, held per debounced/throttled function instance.
Common mistakes
- Using throttle where debounce was actually wanted (or vice versa) — the practical difference (“keep updating periodically” versus “only after it stops”) matters for what the user experiences.
- Creating a new throttled/debounced function on every render/call in a UI framework, losing the closure-held state that makes the technique work at all.
Runtime notes
A coding pattern built on core setTimeout/Date.now(), behaving identically anywhere JavaScript
runs.