tryjs.app

JavaScript Debounce vs Throttle — When to Use Each

Learn the difference between debounce and throttle in JavaScript with runnable examples. When to use each, and how to implement both from scratch.

Debounce vs Throttle

Rate-limiting functions — two common strategies

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

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

const log = label => value => console.log(label, value);
const debounced = debounce(log("debounced:"), 300);
const throttled = throttle(log("throttled:"), 300);

// Simulate rapid events
[0, 50, 100, 150, 200, 500, 900].forEach((ms, i) => {
  setTimeout(() => {
    debounced(i);
    throttled(i);
  }, ms);
});

Overview

Debounce and throttle are two ways to control how often a function runs when events fire rapidly — keystrokes, scroll, resize, mousemove. Debounce waits until events stop: the function runs once after a quiet period. Throttle caps the rate: the function runs at most once every N milliseconds, regardless of how many events fire. Choosing the right one depends on whether you care about the final state (debounce) or sampled updates along the way (throttle).

How It Works

Debounce: run after events stop

Every call resets a timer. The function only fires when the timer elapses without being reset. Result: one call after the user stops typing, scrolling, or resizing.

Throttle: cap to one call per interval

The function runs immediately, then ignores subsequent calls until the interval has passed. Result: regular samples at a fixed rate, regardless of event frequency.

Leading vs trailing edges

Both patterns can be configured to fire at the start of a burst (leading), the end (trailing), or both. Most libraries default debounce to trailing and throttle to leading+trailing.

Cleanup matters

Both implementations schedule timers. If a component unmounts or the listener is removed, cancel any pending timer to avoid calling a stale function with stale state.

Common Mistakes

  • Reaching for debounce when you need sampled updates (like a progress indicator during scroll) — throttle is the correct tool.
  • Forgetting to cancel pending timers on teardown, causing stale calls after unmount.
  • Recreating the debounced function on every render — it has to be the same reference across calls to share its timer.
  • Setting the delay too short (under 100ms) — you get no benefit and still pay the scheduling cost.

When to Use It

Use debounce for 'final state' operations: autocomplete search, form validation after typing stops, saving drafts. Use throttle for 'live feedback' operations: scroll position reporting, mousemove handlers, resize layout updates.

Frequently Asked Questions

What is the difference between debounce and throttle?

Debounce fires once after events stop for a set duration. Throttle fires at most once per interval, regardless of how many events arrive. Debounce cares about the final event; throttle cares about sampling at a fixed rate.

When should I use debounce?

Use debounce when you only care about the final value after a burst — search-as-you-type (wait until typing stops before hitting the API), resize handlers that recompute layout once the window settles, autosave drafts after idle.

When should I use throttle?

Use throttle when you need regular sampled updates during a burst — scroll-position indicators, drag handlers, mousemove tracking, rate-limited API calls where you must send at most N requests per second.

Do I need a library like lodash for debounce and throttle?

No — both can be written in ~10 lines, as shown in the runnable example. Libraries add options (leading/trailing edges, maxWait, cancel methods) that are nice but not essential for simple cases.