Implement Debounce & Throttle
Debounce and throttle are two of the most common machine-coding questions. Both are small closures that control how often a function runs.
Definition
| Debounce | Throttle | |
|---|---|---|
| Idea | Wait until the user stops, then run once | Run at most once every N milliseconds |
| Good for | Search box, auto-save, window resize end | Scroll, mouse move, button spam |
| While events keep coming | Keeps resetting the timer, nothing runs | Runs on a fixed rhythm |
Analogy: an elevator. Debounce is an elevator that waits until people stop walking in before closing the doors. Throttle is an elevator that leaves every 30 seconds no matter how many people keep arriving.
1Events: x x x x x . . . . x x x . . . .2Debounce: . . . . . . . R . . . . . . . R (runs after quiet gap)3Throttle: R . . . R . . . R . . . R . . . (runs on a schedule)Debounce
1function debounce(fn, delay) {2 let timer; // lives in the closure3 return function (...args) {4 clearTimeout(timer); // cancel the previous pending call5 timer = setTimeout(() => {6 fn.apply(this, args); // keep the right this and arguments7 }, delay);8 };9}10 11const onSearch = debounce((q) => fetchResults(q), 300);12input.addEventListener("input", (e) => onSearch(e.target.value));How it works: every call cancels the previous timer and starts a new one. The function only runs if no new call arrives for delay ms. The timer variable is shared between calls through the closure.
Leading edge and cancel
Interviewers often ask for extras.
1function debounce(fn, delay, { leading = false } = {}) {2 let timer;3 function debounced(...args) {4 const callNow = leading && !timer;5 clearTimeout(timer);6 timer = setTimeout(() => {7 timer = null;8 if (!leading) fn.apply(this, args);9 }, delay);10 if (callNow) fn.apply(this, args); // run immediately on the first call11 }12 debounced.cancel = () => { clearTimeout(timer); timer = null; };13 return debounced;14}leading: true runs on the first event and ignores the rest until the quiet gap. That suits a "Submit" button where you want the first click to count.
Throttle
1function throttle(fn, limit) {2 let lastCall = 0;3 return function (...args) {4 const now = Date.now();5 if (now - lastCall >= limit) {6 lastCall = now;7 fn.apply(this, args);8 }9 };10}11 12window.addEventListener("scroll", throttle(updateProgressBar, 200));How it works: remember the time of the last run. If enough time has passed, run again, otherwise ignore the call. This simple version drops the final call. A trailing version also schedules one last run so the final position is not lost.
Here Is What Breaks in React
1function Search() {2 const onChange = debounce((q) => fetchResults(q), 300); // ✗ new debounce every render3 return <input onChange={(e) => onChange(e.target.value)} />;4}Every render creates a new closure with a new timer, so earlier calls are never cancelled and nothing is debounced. Keep one instance across renders.
1function Search() {2 const onChange = useMemo(3 () => debounce((q) => fetchResults(q), 300),4 []5 );6 useEffect(() => () => onChange.cancel?.(), [onChange]); // clean up on unmount7 return <input onChange={(e) => onChange(e.target.value)} />;8}A cleaner pattern is to debounce the value, not the function:
1function useDebounce(value, delay) {2 const [debounced, setDebounced] = useState(value);3 useEffect(() => {4 const id = setTimeout(() => setDebounced(value), delay);5 return () => clearTimeout(id); // cancel when value changes again6 }, [value, delay]);7 return debounced;8}9 10const query = useDebounce(text, 300);11useEffect(() => { fetchResults(query); }, [query]);Here the effect cleanup plays the role of clearTimeout.
Which One to Pick
- Fire on final value: search, autosave, validation, resize end → debounce.
- Fire while it keeps happening, but less often: scroll, drag, mouse move, resize tracking → throttle.
- Visual updates tied to screen refresh: consider
requestAnimationFrameinstead of a timed throttle.
Summary
1Debounce → run after N ms of quiet, each call resets the timer2Throttle → run at most once per N ms3Both → a closure holding a timer or last-run time4Use fn.apply(this, args) → keeps this and arguments5Leading / trailing → run on first event, last event, or both6React → keep one instance with useMemo or useRef, or debounce the value with a hook7Always clear timers on unmountThe last topic in this section shows another favorite coding round: writing built-in methods from scratch.