StackKit

Debounce vs Throttle in JavaScript: Differences and Implementations

By · · 4 min read

Some browser events fire far more often than your code should react to them: every keystroke in a search box, every pixel of a scroll, every frame of a window resize. Debouncing and throttling are two small patterns that limit how often a function runs. They are often mixed up, but they solve different problems.

The difference in one sentence

  • Debounce waits until the events stop for a set time, then runs once.
  • Throttle runs at most once per set time while events keep coming.

An everyday analogy: a lift door that stays open as long as people keep stepping in (debounce) versus a bus that leaves every ten minutes regardless of who is waiting (throttle).

Debounce

Each new call cancels the previous timer and starts a new one. The function runs only after a quiet period.

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

const search = debounce((q) => console.log("debounced search:", q), 50);
search("r");
search("re");
search("rea");
search("reac");
search("react");
// after 50 ms of silence:
// debounced search: react

Five calls produced one execution, with the last arguments. This is what you want for a search-as-you-type box: send one request for the final text instead of five.

Good uses:

  • Search suggestions and autosave while typing.
  • Recalculating layout after a window resize ends.
  • Validating a form field after the user pauses.

Throttle

A throttled function runs immediately, then ignores further calls until the interval has passed.

function throttle(fn, ms) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= ms) {
      last = now;
      fn.apply(this, args);
    }
  };
}

const log = throttle((n) => console.log("throttled call", n), 100);
let i = 0;
const id = setInterval(() => {
  log(++i);
  if (i >= 25) clearInterval(id);
}, 10);

Calling it every 10 ms for 25 ticks, with a 100 ms limit, ran the function at calls 1, 11 and 21, which is about once per 100 ms:

throttled call 1
throttled call 11
throttled call 21

Good uses:

  • Scroll handlers (updating a progress bar, lazy-loading checks).
  • Mouse-move tracking.
  • Rate-limiting button clicks that trigger expensive work.

Leading and trailing edges

Real libraries add options because the simple versions above make trade-offs:

  • Leading edge: run immediately on the first call, then ignore the rest until things settle. Useful for a "Save" button where the first click should act at once.
  • Trailing edge: run after the quiet period (the default for debounce above).

This simple throttle runs on the leading edge only, so the final event in a burst can be dropped. If you need the last value (for example the final scroll position), use a throttle that also schedules a trailing call. Libraries such as Lodash provide _.debounce and _.throttle with leading and trailing options, and they are worth using in production.

For animation, use requestAnimationFrame

If the work is visual (updating positions as the user scrolls), requestAnimationFrame often replaces a time-based throttle because it runs once per screen refresh:

let ticking = false;
window.addEventListener("scroll", () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateProgressBar();
      ticking = false;
    });
    ticking = true;
  }
});

Choosing between them

Situation Use
React to the final value after the user stops debounce
Keep up with a continuous stream at a limited pace throttle
Smooth visual updates tied to scrolling requestAnimationFrame
First action should happen right away leading-edge debounce or throttle

Common mistakes

  • Creating the debounced function inside a component or handler on every render. Each render makes a new timer, so nothing is ever debounced. Create it once (in React, with useMemo, useRef or useCallback).
  • Forgetting to cancel on cleanup. A pending timer can fire after a component unmounts. Libraries expose a .cancel() method for this.
  • Using throttle where debounce fits. Throttling a search box still sends many requests; debouncing sends one.
  • Choosing a delay by habit. 200–300 ms is common for typing, but test what feels right for your interface.

Summary

Debounce waits for calm and then acts once; throttle acts steadily at a capped rate. Decide whether you care about the final value or a regular stream of values, and the choice follows.