Fossils💻 CodingImplement Debounce
🥚EggJavaScriptPerformancePatterns

Implement Debounce

Writing debounce from scratch is a classic senior interview question. But the real test is showing you understand when and why to use it.

Implement Debounce

This is one of the most common coding questions in senior frontend interviews. The implementation is straightforward — the differentiation is in edge cases and real-world application.

Core Implementation

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

How it works: Every call resets the timer. The function only executes after the caller stops calling for delay ms.

Senior-Level Implementation

Production debounce needs more:

function debounce(fn, delay, options = {}) {
  let timeoutId;
  let lastArgs;
  let lastThis;
  let result;
 
  const { leading = false, trailing = true, maxWait } = options;
  let lastCallTime;
  let lastInvokeTime = 0;
 
  function invoke() {
    const args = lastArgs;
    const thisArg = lastThis;
    lastArgs = lastThis = undefined;
    lastInvokeTime = Date.now();
    result = fn.apply(thisArg, args);
    return result;
  }
 
  function startTimer(pendingFn, wait) {
    timeoutId = setTimeout(pendingFn, wait);
  }
 
  function trailingEdge() {
    timeoutId = undefined;
    if (trailing && lastArgs) invoke();
    lastArgs = lastThis = undefined;
  }
 
  function debounced(...args) {
    const time = Date.now();
    const isInvoking = shouldInvoke(time);
 
    lastArgs = args;
    lastThis = this;
    lastCallTime = time;
 
    if (isInvoking) {
      if (timeoutId === undefined && leading) {
        lastInvokeTime = time;
        startTimer(trailingEdge, delay);
        return invoke();
      }
      if (maxWait !== undefined) {
        startTimer(trailingEdge, delay);
        return invoke();
      }
    }
    if (timeoutId === undefined) {
      startTimer(trailingEdge, delay);
    }
    return result;
  }
 
  function shouldInvoke(time) {
    const timeSinceLastCall = time - (lastCallTime ?? 0);
    const timeSinceLastInvoke = time - lastInvokeTime;
    return (
      lastCallTime === undefined ||
      timeSinceLastCall >= delay ||
      (maxWait !== undefined && timeSinceLastInvoke >= maxWait)
    );
  }
 
  debounced.cancel = function () {
    clearTimeout(timeoutId);
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timeoutId = undefined;
  };
 
  debounced.flush = function () {
    if (timeoutId !== undefined) trailingEdge();
    return result;
  };
 
  return debounced;
}

Key Options to Discuss

OptionPurpose
leadingFire on the first call, not the last
trailingFire after the delay (default behavior)
maxWaitGuarantee execution at least every N ms
cancelCancel pending invocations
flushExecute pending invocation immediately

Real-World Usage

function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
 
  const search = useMemo(
    () => debounce(async (q) => {
      const data = await fetchResults(q);
      setResults(data);
    }, 300),
    []
  );
 
  useEffect(() => () => search.cancel(), [search]);
 
  return (
    <input
      value={query}
      onChange={(e) => {
        setQuery(e.target.value);
        search(e.target.value);
      }}
    />
  );
}

Debounce vs. Throttle

  • Debounce: Wait until activity stops, then execute once
  • Throttle: Execute at most once per interval, regardless of activity

Use debounce for: search input, form validation, resize handlers. Use throttle for: scroll handlers, mouse move, API polling.

The interview signal isn't whether you can write debounce — it's whether you know when to use it, when to use throttle, and when neither is appropriate.