On this page

Implement Debounce & Throttle

4 min read

Debounce and throttle are two of the most common machine-coding questions. Both are small closures that control how often a function runs.

Definition

DebounceThrottle
IdeaWait until the user stops, then run onceRun at most once every N milliseconds
Good forSearch box, auto-save, window resize endScroll, mouse move, button spam
While events keep comingKeeps resetting the timer, nothing runsRuns 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

js
1function debounce(fn, delay) {
2 let timer; // lives in the closure
3 return function (...args) {
4 clearTimeout(timer); // cancel the previous pending call
5 timer = setTimeout(() => {
6 fn.apply(this, args); // keep the right this and arguments
7 }, 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.

js
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 call
11 }
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

js
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

jsx
1function Search() {
2 const onChange = debounce((q) => fetchResults(q), 300); // ✗ new debounce every render
3 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.

jsx
1function Search() {
2 const onChange = useMemo(
3 () => debounce((q) => fetchResults(q), 300),
4 []
5 );
6 useEffect(() => () => onChange.cancel?.(), [onChange]); // clean up on unmount
7 return <input onChange={(e) => onChange(e.target.value)} />;
8}

A cleaner pattern is to debounce the value, not the function:

jsx
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 again
6 }, [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 requestAnimationFrame instead of a timed throttle.

Summary

1Debounce → run after N ms of quiet, each call resets the timer
2Throttle → run at most once per N ms
3Both → a closure holding a timer or last-run time
4Use fn.apply(this, args) → keeps this and arguments
5Leading / trailing → run on first event, last event, or both
6React → keep one instance with useMemo or useRef, or debounce the value with a hook
7Always clear timers on unmount

The last topic in this section shows another favorite coding round: writing built-in methods from scratch.

Next TopicPolyfills: map, bind, Promise.all