← Back to Home
JavaScript

Debounce and Throttle in JavaScript

Kuligaposten

Debounce and Throttle in JavaScript

Search boxes that fire a request on every keystroke. Scroll handlers that run hundreds of times a second. Resize listeners that recalculate a layout on every single pixel change. All of these have the same problem: an event fires far more often than your code actually needs to react to it.

Debouncing and throttling are the two standard fixes. They solve slightly different problems, and mixing them up is a common mistake.

Debounce: wait for the pause

A debounced function only runs after a burst of calls has stopped for a given delay. Every new call resets the timer. This is exactly what you want for a search-as-you-type input — you don’t want to fire a request after every keystroke, only once the user has actually paused typing.

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

Usage:

const input = document.querySelector('#search');
const handleSearch = debounce((event) => {
  console.log('Searching for:', event.target.value);
}, 300);

input.addEventListener('input', handleSearch);

Type continuously and nothing happens until you stop for 300ms. That’s the whole point — the function only cares about the final state, not every intermediate one.

Throttle: run at most once per interval

A throttled function runs at a steady, capped rate no matter how often it’s called — once every delay milliseconds, guaranteed. This fits scroll and resize handlers, where you still want regular updates while the event is happening, just not on every single firing.

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

Usage:

const handleScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 200);

window.addEventListener('scroll', handleScroll);

Scroll continuously and the handler still fires — just at most every 200ms, instead of on every scroll event the browser dispatches.

Which one do I want?

  • Debounce — you only care about the final value after activity stops: search inputs, form validation, auto-save, window resize where you only need the end result.
  • Throttle — you need regular updates while activity is ongoing: infinite scroll, drag handlers, mouse-move tracking, scroll-position-based animations.

A quick way to remember it: debounce says “wait until they’re done,” throttle says “keep me updated, just not too often.”

A note on this and cleanup

Both implementations above use fn.apply(this, args) so the wrapped function still receives the correct this and arguments when called as an event handler or object method. If you’re removing the listener later, keep a reference to the debounced/throttled function itself — element.removeEventListener('scroll', throttle(fn, 200)) won’t work, because that creates a new throttled function that was never attached in the first place.

const handleScroll = throttle(onScroll, 200);
window.addEventListener('scroll', handleScroll);
// later
window.removeEventListener('scroll', handleScroll);

Most UI libraries and utility packages (Lodash’s debounce/throttle being the most common) ship more configurable versions of these — with options for leading/trailing calls, max wait times, and cancellation. But the two functions above cover the common case and are worth understanding from scratch before reaching for a dependency.

Related Topics

← Back to Home