Q35 of 40 · JavaScript

How do you implement debounce and throttle, and when do you use each?

JavaScriptSeniorjavascriptdebouncethrottleperformanceclosurestimers

Short answer

Short answer: Debounce delays execution until a period of inactivity — the function runs after the last call if no new call arrives within the delay. Throttle limits execution to once per interval regardless of how many times it's called. Debounce for search input; throttle for scroll/resize handlers.

Detail

Both patterns limit how often a function executes in response to high-frequency events.

Debounce: Resets a timer on every call. The function executes only after the last call's delay expires without another call. Use for: search-as-you-type (wait until typing stops), window resize (recalculate after resize finishes), auto-save.

Throttle: Executes at most once per interval. Intermediate calls during the interval are dropped (or queued for the next window). Use for: scroll event handlers, rate-limiting API calls from button spam, game loop input.

Implementation details:

  • Both use closures over a timer ID or last-called timestamp.
  • Leading-edge vs trailing-edge: debounce can fire immediately (leading) then ignore subsequent calls, or fire after inactivity (trailing, the default).
  • Lodash _.debounce and _.throttle handle edge cases: maxWait, cancellation, flushing.

In test automation: Debounce and throttle in the tested UI require extra care in Playwright — you may need to wait for the debounce timeout before asserting the result. Playwright's page.waitForResponse or explicit page.waitForTimeout(delay) after triggering input handles this.

// EXAMPLE

// Debounce — trailing edge
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
const search = debounce(query => fetchResults(query), 300);
input.addEventListener("input", e => search(e.target.value));

// Throttle — trailing interval
function throttle(fn, interval) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}
window.addEventListener("scroll", throttle(() => updatePosition(), 100));

// Playwright — wait for debounce to settle
await page.fill("#search", "playwright");
await page.waitForTimeout(350); // wait past debounce delay
await expect(page.locator(".results")).toBeVisible();

// WHAT INTERVIEWERS LOOK FOR

Clear distinction between debounce (inactivity) and throttle (rate). Correct closure-based implementation. The leading vs trailing edge distinction. Connecting to test timing — debounced UI elements need extra wait time in tests.

// COMMON PITFALL

Confusing debounce with throttle. Forgetting that the returned function must preserve `this` — arrow functions inside the implementation capture `this` from the outer scope instead of the caller, breaking method usage.