How to implement throttling
Throttle = call the function at most once per N ms regardless of how often the trigger fires. Two flavors: leading (fire immediately, then ignore until cooldown ends) and trailing (fire on the last call within the window). Track lastCall timestamp; compare to now; schedule a trailing call if needed.
Throttle and debounce solve different problems. Throttle guarantees a minimum spacing between calls — useful for high-frequency events you want to sample, not skip.
The core idea
Throttle: at most one call every N ms. Debounce: collapse a burst to a single call after the burst ends.
Throttle for scroll, mousemove, resize, drag — you want regular updates, not just one at the end.
Implementation — leading + trailing
function throttle(fn, wait) {
let lastCall = 0;
let timer = null;
let lastArgs = null;
return function (...args) {
const now = Date.now();
const remaining = wait - (now - lastCall);
if (remaining <= 0) {
// leading: fire immediately
if (timer) { clearTimeout(timer); timer = null; }
lastCall = now;
fn.apply(this, args);
} else {
// trailing: schedule the last call to fire at end of window
lastArgs = args;
if (!timer) {
timer = setTimeout(() => {
lastCall = Date.now();
timer = null;
fn.apply(this, lastArgs);
}, remaining);
}
}
};
}Flavors
- Leading-only — fire on the first call, ignore rest until cooldown.
- Trailing-only — fire only at the end of a window.
- Both (above) — fire immediately, and ensure the last call within the window also fires; this is what users usually want.
requestAnimationFrame as a throttle
For scroll / mousemove that updates the UI, prefer rAF-throttling: at most one update per frame.
function rafThrottle(fn) {
let queued = false, lastArgs;
return function (...args) {
lastArgs = args;
if (queued) return;
queued = true;
requestAnimationFrame(() => {
queued = false;
fn.apply(this, lastArgs);
});
};
}When to use throttle vs debounce
- Throttle: scroll position updates, drag, resize-driven layout, analytics sampling, API polling rate cap.
- Debounce: search-as-you-type, form validation, save-on-stop-typing.
Interview framing
"Throttle gives a minimum spacing between calls — at most one every N ms. The standard implementation tracks the last call timestamp; if we're past the cooldown, fire immediately (leading); otherwise schedule the most recent args to fire at the end of the window (trailing). For scroll/mousemove that drives UI, I'd actually use rAF-throttling so updates align with frames. Throttle is for sampling a high-frequency stream; debounce is for waiting until the burst ends."
Follow-up questions
- •Difference between throttle and debounce, with use cases for each.
- •Why prefer rAF-throttling over time-based throttling for scroll handlers?
- •How would you cancel a pending trailing call on unmount?
Common mistakes
- •Confusing throttle with debounce — opposite UX.
- •No trailing call — last user action is lost.
- •Throttling scroll with setTimeout when rAF aligns better with rendering.
- •Memory leak from never clearing the trailing timer on unmount.
Performance considerations
- •Throttle bounds work per unit time — critical on scroll where naive handlers can fire 100×/sec. rAF-throttle bounds to vsync — at most ~60/sec — and avoids out-of-frame work.
Edge cases
- •Rapid burst then stop — must trailing-fire so final state is correct.
- •Component unmount mid-window — clear timer.
- •this/args binding when wrapping methods.
Real-world examples
- •Infinite-scroll position checks.
- •Drag handles in a chart.
- •Resize-driven canvas redraws.
- •Lodash `_.throttle`.