Throttle vs Debounce in JavaScript

Interactive pages fire lots of events (scroll, resize, input, mousemove). Running your code on every event can be slow. Two small helpers—debounce and throttle—let you control how often your function runs.

Quick definitions

Mental model

When to use which

Side-by-side

Debounce Throttle
Runs After no events for X ms At most once per X ms
Good for “Wait until user stops” “Update at a steady rate”
Typical events input, keyup, change scroll, resize, mousemove

Code you can use

Debounce (with optional immediate/leading call)

function debounce(callback, delay, immediate = false) {
  let timeout = null;
  return function (...args) {
    const shouldCallNow = immediate && !timeout;
    if (shouldCallNow) callback.apply(this, args);
    timeout && clearTimeout(timeout);
    timeout = setTimeout(() => {
      if (!immediate) callback.apply(this, args);
      timeout = null;
    }, delay);
  };
}

Use it like:

const searchInput = document.querySelector('#search');

const onType = debounce((e) => {
  const q = e.target.value.trim();
  if (!q) return;
  // Fetch after user stops typing for 300ms
  fetch(`/search?q=${encodeURIComponent(q)}`).then(/* ... */);
}, 300);

searchInput.addEventListener('input', onType);

Throttle

function throttle(callback, delay) {
  let timeout = null;
  let lastExecTime = 0;
  function callExec(context, args) {
    callback.apply(context, args);
    lastExecTime = Date.now();
  }
  function throttler(...args) {
    const now = Date.now();
    const timeSinceLastExec = now - lastExecTime;
    const remainingDelay = delay - timeSinceLastExec;
    if (timeSinceLastExec >= delay) {
      callExec(this, args);
    } else {
      if (timeout) clearTimeout(timeout);
      timeout = setTimeout(() => {
        callExec(this, args);
      }, remainingDelay);
    }
  }
  throttler.cancel = () => {
    timeout && clearTimeout(timeout);
    timeout = null;
  };
  return throttler;
}

Use it like:

const onScroll = throttle(() => {
  // Runs at most once every 100ms even if scroll fires constantly
  console.log(window.scrollY);
}, 100);

window.addEventListener('scroll', onScroll);

// You can cancel a pending trailing call if needed
// onScroll.cancel();

Choosing quickly

Gotchas and tips