Back to JavaScript
JavaScript
medium
very high
mid

How would you implement debounce and throttle, and when do you use each?

Debounce delays the call until activity stops; throttle caps how often the call can fire. Both control noisy events but solve different problems.

6 min read·~15 min to think through

Debounce — fires once after the input has been quiet for N ms. Use for: search-as-you-type, autosave on form input, window-resize end. The user sees no work happen until they pause.

Throttle — fires at most once per N ms while activity continues. Use for: scroll handlers, mouse-move tracking, drag, analytics. The user sees regular updates while they keep going.

Mental model: debounce waits for silence, throttle paces the signal.

Implementation traps to mention:

  • leading vs trailing edge — does the first call fire immediately or only after the wait?
  • Cancellation — return a .cancel() so React effects can clean up.
  • Preserving this and arguments of the call.

Code

ts
function debounce<T extends (...a: any[]) => any>(fn: T, wait = 200) {
  let t: ReturnType<typeof setTimeout> | null = null;
  function debounced(this: any, ...args: Parameters<T>) {
    if (t) clearTimeout(t);
    t = setTimeout(() => fn.apply(this, args), wait);
  }
  debounced.cancel = () => { if (t) { clearTimeout(t); t = null; } };
  return debounced as T & { cancel: () => void };
}
Debounce — trailing edge with cancel()
ts
function throttle<T extends (...a: any[]) => any>(fn: T, wait = 200) {
  let last = 0, t: ReturnType<typeof setTimeout> | null = null, lastArgs: any[] = [];
  return function (this: any, ...args: Parameters<T>) {
    const now = Date.now();
    const remaining = wait - (now - last);
    lastArgs = args;
    if (remaining <= 0) {
      if (t) { clearTimeout(t); t = null; }
      last = now;
      fn.apply(this, args);
    } else if (!t) {
      t = setTimeout(() => {
        last = Date.now();
        t = null;
        fn.apply(this, lastArgs);
      }, remaining);
    }
  };
}
Throttle — leading edge, with trailing flush

Follow-up questions

  • When would you use both debounce and a maxWait together?
  • How does requestAnimationFrame compare to a 16ms throttle for scroll handlers?
  • How do you correctly debounce inside a React component (closure pitfalls)?

Common mistakes

  • Calling debounce/throttle inside render — every render creates a new instance, so the timer never accumulates.
  • Forgetting to cancel on unmount — the timer fires after the component is gone and crashes setState.
  • Confusing the two: using debounce for scroll (jumps after pause) or throttle for search (extra requests).

Performance considerations

  • For 60fps UI work tied to events, prefer `requestAnimationFrame` over a 16ms throttle — it aligns with frames and pauses on hidden tabs.
  • Throttling DOM-touching handlers prevents layout thrashing; debouncing them delays the cost.

Edge cases

  • Page-hidden tabs (`visibilitychange`) freeze setTimeout to a coarse cadence — throttle accuracy suffers.
  • Trailing-edge debounce on unmount can fire after the component is gone if you don't cancel.

Real-world examples

  • Search-as-you-type with a 300ms debounce avoids one API call per keystroke.
  • Infinite-scroll position checks throttled to 100ms keep CPU calm while preserving smoothness.

Senior engineer discussion

At senior level discuss leading+trailing semantics, maxWait (lodash), and how you'd test these (fake timers + flush).

Related questions