Throttle and Debounce

Two techniques for limiting how often expensive work runs in response to a high-frequency event — throttle caps the rate, debounce waits for a pause.

                throttle(fn, intervalMs) / debounce(fn, delayMs)
              

Purpose

A high-frequency event (scroll, resize, input) can fire far more often than the resulting work actually needs to run. Throttle and debounce both limit how often a function runs, but for different intents.

Throttle

function throttle(fn, intervalMs) {
  let lastRun = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastRun >= intervalMs) {
      lastRun = now;
      fn(...args);
    }
  };
}

window.addEventListener('scroll', throttle(updateScrollIndicator, 100));

Runs at most once per intervalMs, regardless of how often it’s triggered — suited to “keep this roughly continuously updated, but not on every single event.”

Debounce

function debounce(fn, delayMs) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delayMs);
  };
}

searchInput.addEventListener('input', debounce(runSearch, 300));

Waits for a pause of delayMs with no new trigger before running once — suited to “only run once the user has stopped” (search-as-you-type, resize-triggered layout recalculation).

Behavior notes

  • Throttle guarantees periodic execution during continuous triggering; debounce can indefinitely postpone execution if triggering never pauses long enough.
  • Some debounce implementations offer a “leading edge” option (run immediately on the first trigger, then debounce subsequent ones) for cases where an instant first response matters.
  • Both are typically implemented as closures — the “last run” or “pending timeout” state is private, held per debounced/throttled function instance.

Common mistakes

  • Using throttle where debounce was actually wanted (or vice versa) — the practical difference (“keep updating periodically” versus “only after it stops”) matters for what the user experiences.
  • Creating a new throttled/debounced function on every render/call in a UI framework, losing the closure-held state that makes the technique work at all.

Runtime notes

A coding pattern built on core setTimeout/Date.now(), behaving identically anywhere JavaScript runs.

Taught in these lessons

Updated 2026-09-03