Debouncing, throttling, and other real event-handling patterns

Some browser events fire far more often than any handler can usefully respond to — a scroll or resize can fire dozens of times per second — and debounce and throttle are two genuinely different strategies for taming that rate, each right for a different real situation.

Advanced

3 min read

The problem: some events fire far too often to handle directly

window.addEventListener("resize", () => {
  recalculateLayout(); // this expensive function might run 30-60+ times per SECOND during a drag-resize
});

Events like scroll, resize, and input (on a fast typist) can fire dozens of times per second — running an expensive handler (a layout recalculation, an API call) on every single firing is both wasteful and can visibly degrade performance, especially if the handler itself takes longer than the gap between firings. Debounce and throttle are two different, real strategies for reducing how often the expensive work actually runs, without losing the responsiveness of reacting to the event at all.

Debounce: wait for a pause, then run once

function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId); // cancel any PENDING call — restart the wait
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}
 
const debouncedSearch = debounce((query) => searchApi(query), 300);
input.addEventListener("input", (e) => debouncedSearch(e.target.value));
// Typing "hello" fires 5 input events, but searchApi only runs ONCE, 300ms after the LAST keystroke

Debounce resets a timer on every single call, so the wrapped function only actually runs once the calls stop for the full delay period — every call within that window cancels and restarts the wait. This is exactly right for a search-as-you-type box: the goal isn't "run on every keystroke," it's "run once the user has actually paused typing," and debounce is the direct implementation of that specific intent.

Throttle: run at most once per fixed interval, regardless of call frequency

function throttle(fn, interval) {
  let lastRun = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastRun >= interval) {
      lastRun = now;
      fn(...args); // runs immediately if enough time has passed since the LAST actual run
    }
  };
}
 
const throttledScroll = throttle(() => updateScrollIndicator(), 100);
window.addEventListener("scroll", throttledScroll);
// Scrolling continuously still updates the indicator regularly, but never more than 10x/second

Throttle guarantees the wrapped function runs at a bounded maximum rate — no more than once per interval — but unlike debounce, it doesn't wait for calls to stop; it runs periodically throughout continuous activity. This is the right choice when ongoing feedback during the activity matters (a scroll-position indicator should update while scrolling, not only once scrolling stops), which debounce would handle badly, since debounce would show nothing until scrolling paused entirely.

Why picking the wrong one produces a genuinely bad user experience

Using debounce for scroll-position tracking means the indicator only updates once scrolling stops — visibly frozen and unresponsive during the actual scroll, which defeats the point of a live indicator. Using throttle for search-as-you-type means a request fires on a fixed schedule regardless of whether the user is still actively typing, wasting requests on incomplete queries the user hasn't finished typing yet. The two patterns aren't interchangeable performance knobs with the same effect at different settings — they express genuinely different intents (wait for a pause vs. maintain a steady rate), and choosing between them should follow from which intent actually matches the UI behavior needed.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What does debounce actually do, mechanically?

2. How does throttle differ from debounce?

3. Why does using debounce for scroll-position tracking produce a bad user experience?