Skip to content

Debounce vs throttle in JavaScript, with real examples

· 3 min read · Amrith Vengalath

  • JavaScript
  • Performance
  • Web

I mixed these two up for an embarrassingly long time. They sound similar, they both "slow things down," and a lot of tutorials use them almost interchangeably. They're not the same, and picking the wrong one gives you a search box that feels laggy or a scroll handler that never seems to fire when you want it to.

Here's the version of this explanation I wish someone had given me.

The one-line difference

  • Debounce: wait until the user stops doing the thing, then run once.
  • Throttle: run at most once every N milliseconds while the thing keeps happening.

Debounce cares about the pause. Throttle cares about the interval.

The classic case is a search-as-you-type box that hits an API. Without debounce, typing "shoes" fires five requests - one per keystroke. You want one request, after the person stops typing.

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
 
const search = debounce((query) => {
  fetch(`/api/search?q=${encodeURIComponent(query)}`);
}, 300);
 
input.addEventListener("input", (e) => search(e.target.value));

Every keystroke resets the timer. The fetch only fires once the user pauses for 300ms. Five keystrokes in quick succession produce exactly one request.

Throttle: the scroll handler

Now imagine a scroll listener that updates a progress bar or checks whether to load more content. Scroll events fire absurdly often - dozens per second. You don't want to wait for the user to stop scrolling (debounce would mean the bar only updates when they let go, which feels broken). You want regular updates, just not on every single event.

function throttle(fn, interval) {
  let lastRun = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastRun >= interval) {
      lastRun = now;
      fn.apply(this, args);
    }
  };
}
 
const onScroll = throttle(() => {
  updateScrollProgress();
}, 100);
 
window.addEventListener("scroll", onScroll);

This runs at most ten times a second no matter how furiously someone scrolls. Smooth enough to look live, cheap enough not to jank.

How to pick

I keep it to one question: do I want the action to wait for a pause, or to happen at a steady rate?

  • Search input, resize-then-recalculate, "save draft after they stop typing" → debounce.
  • Scroll, mousemove, drag, anything you want to feel continuous but rate-limited → throttle.

A trap I fell into early: using throttle for the search box "because it's about performance." It technically reduces requests, but you still fire mid-word, which is wasteful and the results flicker. Debounce is the right tool there. The goal isn't just "fewer calls," it's the right calls.

A note on using a library

These implementations are fine for understanding, and honestly fine for small projects. But the versions in a library like Lodash handle edge cases mine skip - leading vs trailing execution, cancel methods, max wait times. On a real codebase I'd usually reach for lodash.debounce rather than hand-roll it, mostly so the next person reading the code recognizes it instantly. Knowing how they work, though, is what lets you choose correctly in the first place.