Timing Control

Med

Debounce and throttle are essential patterns for controlling function execution frequency. Debounce delays execution until activity stops; throttle limits execution rate. Both use closures to maintain timer state.

Interactive Visualization

CodeSetup
1function debounce(fn, wait) {
2 let timeoutId;
3 return function(...args) {
4 clearTimeout(timeoutId);
5 timeoutId = setTimeout(
6 () => fn.apply(this, args),
7 wait
8 );
9 };
10}
11
12// Usage: search input
13const search = debounce(query => {
14 fetch(`/api?q=${query}`);
15}, 300);
Timeline
0mstime →
CallExecuteCancelled
Closure State
timeoutId:null
Output
debounce() creates closure with timeoutId variable
1 / 6
Key Insight: Debounce waits for a "pause" in activity. Each new call resets the timer. Only the LAST call executes.

Key Points

  • Debounce: waits for pause in calls before executing (e.g., search input)
  • Throttle: executes at most once per interval (e.g., scroll handler)
  • Both use closures to persist timer state between calls
  • Leading edge: execute immediately on first call
  • Trailing edge: execute after the wait period
  • Cancel methods allow cleanup on unmount

Code Examples

Basic Debounce

function debounce(fn, wait) {
  let timeoutId;

  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), wait);
  };
}

// Usage: search input
const searchInput = document.getElementById('search');
const handleSearch = debounce((query) => {
  fetch(`/api/search?q=${query}`);
}, 300);

searchInput.addEventListener('input', (e) => {
  handleSearch(e.target.value);
});
// Only searches 300ms after user stops typing

Each call resets the timer; function runs after activity stops

Debounce with Leading/Trailing

function debounce(fn, wait, options = {}) {
  let timeoutId;
  let lastArgs;
  const { leading = false, trailing = true } = options;

  return function(...args) {
    const shouldCallNow = leading && !timeoutId;
    lastArgs = args;

    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      timeoutId = null;
      if (trailing && lastArgs) {
        fn.apply(this, lastArgs);
      }
    }, wait);

    if (shouldCallNow) {
      fn.apply(this, args);
    }
  };
}

// Leading: execute immediately, then debounce
const saveNow = debounce(save, 1000, { leading: true, trailing: false });

// Trailing (default): wait for pause, then execute
const saveAfter = debounce(save, 1000, { leading: false, trailing: true });

Leading fires immediately; trailing fires after wait period

Basic Throttle

function throttle(fn, wait) {
  let lastTime = 0;

  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= wait) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// Usage: scroll handler
const handleScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 100);

window.addEventListener('scroll', handleScroll);
// Logs at most every 100ms while scrolling

Throttle ensures function runs at most once per interval

Throttle with Leading/Trailing

function throttle(fn, wait, options = {}) {
  let lastTime = 0;
  let timeoutId;
  const { leading = true, trailing = true } = options;

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

    if (remaining <= 0 || remaining > wait) {
      if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      lastTime = now;
      if (leading || lastTime !== 0) {
        fn.apply(this, args);
      }
    } else if (!timeoutId && trailing) {
      timeoutId = setTimeout(() => {
        lastTime = leading ? Date.now() : 0;
        timeoutId = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

// Trailing ensures final call executes after scrolling stops
const handleResize = throttle(recalculate, 200, { trailing: true });

Trailing ensures the final invocation is not lost

Debounce with Cancel

function debounce(fn, wait) {
  let timeoutId;

  function debounced(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), wait);
  }

  debounced.cancel = function() {
    clearTimeout(timeoutId);
    timeoutId = null;
  };

  return debounced;
}

// React usage with cleanup
function SearchComponent() {
  const debouncedSearch = useMemo(
    () => debounce(search, 300),
    []
  );

  useEffect(() => {
    return () => debouncedSearch.cancel();  // Cleanup on unmount
  }, [debouncedSearch]);

  return <input onChange={e => debouncedSearch(e.target.value)} />;
}

Cancel method prevents stale executions after unmount

Comparison: Debounce vs Throttle

// User types: a...b...c...d (each 50ms apart)
// wait = 200ms

// DEBOUNCE: waits for pause, then executes ONCE
// Timeline: a--b--c--d-------[execute with 'd']
// Calls: 0, 0, 0, 0, 0, 0, 0, 1

// THROTTLE: executes every 200ms
// Timeline: a--b--c--d--[exec]--[exec]
// Calls: 1, 0, 0, 0, 1, 0, 0, 1

// Use DEBOUNCE for:
// - Search input (wait for user to stop typing)
// - Window resize (recalculate after resizing stops)
// - Auto-save (save after user stops editing)

// Use THROTTLE for:
// - Scroll events (update UI at fixed rate)
// - Mouse move (track position without overwhelming)
// - API rate limiting (max N calls per second)

Debounce: wait for pause. Throttle: limit rate.

Common Mistakes

  • Confusing debounce (wait for pause) with throttle (rate limit)
  • Forgetting to cancel debounced functions on component unmount
  • Not preserving "this" context in the returned function
  • Using debounce when throttle is more appropriate (and vice versa)

Interview Tips

  • Implement debounce from scratch with cancel method
  • Implement throttle from scratch
  • Explain when to use debounce vs throttle with examples
  • Know how leading and trailing edge options work

Practice Problems

Apply this concept by solving these 2 problems

Related Concepts