Debounce
Master the debounce technique in JavaScript to control function execution frequency. Essential for optimizing search inputs, resize handlers, and preventing excessive API calls.
JavaScript · Closures · Async — Pratik Rai ·
Implement throttle from scratch: guarantee a function runs at most once per interval regardless of how often it is called.
Throttling caps how often a function may run. Given a stream of events arriving faster than you can usefully handle them, throttle lets a steady trickle through and discards the rest. It is the right tool for scroll, resize, mousemove and drag — anywhere you need updates during continuous activity rather than after it stops.
The distinction matters more than any implementation detail:
calls: ●●●●●●●●●●●●●●●●●●●●
debounce: ▼ (once, after the burst)
throttle: ▼ ▼ ▼ ▼ ▼ (steady, during the burst)
Debounce answers "tell me when they stop." Throttle answers "tell me regularly while they continue."
Debouncing a scroll handler produces no updates at all until scrolling ends — so a scroll-linked progress bar would sit frozen and then jump. Throttling a search input sends a request every N milliseconds whether the query is finished or not. Choosing wrong produces behaviour that is subtly annoying rather than obviously broken, which is why it survives code review.
Timestamp-based — fires immediately, then suppresses until the interval passes:
JSfile.javascript1function throttle(fn, interval = 200) { 2 let lastCall = 0; 3 4 return function throttled(...args) { 5 const now = Date.now(); 6 if (now - lastCall >= interval) { 7 lastCall = now; 8 return fn.apply(this, args); 9 } 10 }; 11}
Simple and leading-edge, but it drops the final call. If the user stops scrolling mid-interval, the last position is never reported — and the last position is usually the one you care about.
Timer-based — fires on the trailing edge:
JSfile.javascript1function throttle(fn, interval = 200) { 2 let timeoutId = null; 3 let lastArgs = null; 4 5 return function throttled(...args) { 6 lastArgs = args; 7 if (timeoutId !== null) return; 8 9 timeoutId = setTimeout(() => { 10 timeoutId = null; 11 fn.apply(this, lastArgs); 12 }, interval); 13 }; 14}
This captures the trailing call but delays the first one, so the UI feels laggy on the initial event.
Most real uses want both edges: respond immediately, and guarantee the final state is reported.
JSfile.javascript1function throttle(fn, interval = 200, { leading = true, trailing = true } = {}) { 2 let lastCall = 0; 3 let timeoutId = null; 4 let lastArgs = null; 5 let lastThis = null; 6 7 function invoke(time) { 8 lastCall = time; 9 timeoutId = null; 10 fn.apply(lastThis, lastArgs); 11 lastArgs = lastThis = null; 12 } 13 14 function throttled(...args) { 15 const now = Date.now(); 16 if (lastCall === 0 && !leading) lastCall = now; 17 18 const remaining = interval - (now - lastCall); 19 lastArgs = args; 20 lastThis = this; 21 22 if (remaining <= 0) { 23 if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } 24 invoke(now); 25 } else if (trailing && timeoutId === null) { 26 timeoutId = setTimeout(() => invoke(Date.now()), remaining); 27 } 28 } 29 30 throttled.cancel = () => { 31 clearTimeout(timeoutId); 32 timeoutId = null; 33 lastCall = 0; 34 lastArgs = lastThis = null; 35 }; 36 37 return throttled; 38}
Scheduling the trailing call for remaining rather than the full interval is what keeps the cadence even. Naively using interval stretches the gap after every suppressed call and the rate drifts.
Throttle is a general-purpose rate limiter, but for anything that paints to the screen, requestAnimationFrame is strictly better:
JSfile.javascript1function rafThrottle(fn) { 2 let frameId = null; 3 return function (...args) { 4 if (frameId !== null) return; 5 frameId = requestAnimationFrame(() => { 6 frameId = null; 7 fn.apply(this, args); 8 }); 9 }; 10}
This synchronises with the browser's paint cycle instead of guessing at an interval, so you never compute a layout update that gets discarded before it renders. It also pauses automatically in background tabs. For scroll handlers that move elements, this is the correct default — a 16ms throttle is an approximation of what rAF does exactly.
And for the specific case of "tell me when this element enters the viewport", neither is right: IntersectionObserver does it natively with no scroll handler at all.
Date.now() versus performance.now(). Date.now() follows the system clock and can jump backwards on an NTP correction, producing a negative remaining and a burst of calls. performance.now() is monotonic. For a UI throttle the difference never matters; for anything measuring elapsed time it does.
Cancel on unmount. Same as debounce — a pending trailing call that fires after teardown is a bug waiting to happen.
Memoise in React. Creating the throttled function inside the render body gives every render its own timer, which means no throttling at all. useMemo with an empty dependency array, and cancel in the effect cleanup.
Passive listeners. Throttling does not stop a scroll handler blocking the compositor. Add { passive: true } to the listener so the browser knows you will not call preventDefault.
requestAnimationFrame instead for anything visual.cancel, memoise in React, and mark scroll listeners passive.Goal: Implement throttle (leading edge). Bonus: add trailing-call capture with the timestamp approach.
Continue learning with these related challenges
Master the debounce technique in JavaScript to control function execution frequency. Essential for optimizing search inputs, resize handlers, and preventing excessive API calls.
JavaScript · Closures · Async — Pratik Rai ·
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Master the debounce technique in JavaScript to control function execution frequency. Essential for optimizing search inputs, resize handlers, and preventing excessive API calls.
JavaScript · Closures · Async
Pratik Rai ·
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills
Pratik Rai ·
Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.
JavaScript · Arrays · Polyfills
Pratik Rai ·