Back to JavaScript
JavaScript
medium
mid

How would you implement throttling in JavaScript?

Throttle = call the function at most once per N ms regardless of how often the trigger fires. Two flavors: leading (fire immediately, then ignore until cooldown ends) and trailing (fire on the last call within the window). Track lastCall timestamp; compare to now; schedule a trailing call if needed.

3 min read·~15 min to think through

Throttle and debounce solve different problems. Throttle guarantees a minimum spacing between calls — useful for high-frequency events you want to sample, not skip.

The core idea

Throttle: at most one call every N ms. Debounce: collapse a burst to a single call after the burst ends.

Throttle for scroll, mousemove, resize, drag — you want regular updates, not just one at the end.

Implementation — leading + trailing

js
function throttle(fn, wait) {
  let lastCall = 0;
  let timer = null;
  let lastArgs = null;

  return function (...args) {
    const now = Date.now();
    const remaining = wait - (now - lastCall);

    if (remaining <= 0) {
      // leading: fire immediately
      if (timer) { clearTimeout(timer); timer = null; }
      lastCall = now;
      fn.apply(this, args);
    } else {
      // trailing: schedule the last call to fire at end of window
      lastArgs = args;
      if (!timer) {
        timer = setTimeout(() => {
          lastCall = Date.now();
          timer = null;
          fn.apply(this, lastArgs);
        }, remaining);
      }
    }
  };
}

Flavors

  • Leading-only — fire on the first call, ignore rest until cooldown.
  • Trailing-only — fire only at the end of a window.
  • Both (above) — fire immediately, and ensure the last call within the window also fires; this is what users usually want.

requestAnimationFrame as a throttle

For scroll / mousemove that updates the UI, prefer rAF-throttling: at most one update per frame.

js
function rafThrottle(fn) {
  let queued = false, lastArgs;
  return function (...args) {
    lastArgs = args;
    if (queued) return;
    queued = true;
    requestAnimationFrame(() => {
      queued = false;
      fn.apply(this, lastArgs);
    });
  };
}

When to use throttle vs debounce

  • Throttle: scroll position updates, drag, resize-driven layout, analytics sampling, API polling rate cap.
  • Debounce: search-as-you-type, form validation, save-on-stop-typing.

Interview framing

"Throttle gives a minimum spacing between calls — at most one every N ms. The standard implementation tracks the last call timestamp; if we're past the cooldown, fire immediately (leading); otherwise schedule the most recent args to fire at the end of the window (trailing). For scroll/mousemove that drives UI, I'd actually use rAF-throttling so updates align with frames. Throttle is for sampling a high-frequency stream; debounce is for waiting until the burst ends."

Follow-up questions

  • Difference between throttle and debounce, with use cases for each.
  • Why prefer rAF-throttling over time-based throttling for scroll handlers?
  • How would you cancel a pending trailing call on unmount?

Common mistakes

  • Confusing throttle with debounce — opposite UX.
  • No trailing call — last user action is lost.
  • Throttling scroll with setTimeout when rAF aligns better with rendering.
  • Memory leak from never clearing the trailing timer on unmount.

Performance considerations

  • Throttle bounds work per unit time — critical on scroll where naive handlers can fire 100×/sec. rAF-throttle bounds to vsync — at most ~60/sec — and avoids out-of-frame work.

Edge cases

  • Rapid burst then stop — must trailing-fire so final state is correct.
  • Component unmount mid-window — clear timer.
  • this/args binding when wrapping methods.

Real-world examples

  • Infinite-scroll position checks.
  • Drag handles in a chart.
  • Resize-driven canvas redraws.
  • Lodash `_.throttle`.

Senior engineer discussion

Seniors distinguish throttle vs debounce by intent (sample vs collapse), use rAF-throttling for render-driven handlers, and remember to cancel trailing calls on unmount.

Related questions