DEV Community

Devin
Devin

Posted on

Écrire un article complet sur les algorhytme en JavaScriptavec exemples de code, explications claires et cas d'usage réels. - 1792338198507

Debouncing vs. Throttling in JavaScript: How to Choose

JavaScript interfaces often receive events faster than they need to perform useful work. A search field emits an input event for every character. A scroll listener may run many times during a single movement, and dragging a slider produces a stream of changing values. If each event triggers an expensive calculation, layout update, or network request, the application can waste resources and feel sluggish.

Debouncing and throttling both limit how often a callback runs, but they answer different questions. Debouncing asks whether activity has paused long enough to act. Throttling asks whether enough time has passed to act again. That distinction determines which technique suits an interaction.

Debouncing: wait for a pause

A trailing-edge debounce starts a timer whenever its wrapper is called. Each new call clears and restarts that timer. The callback runs after the specified quiet period has passed with no new calls, typically once with the arguments from the latest call.

Imagine a search box configured with a 300-millisecond delay. If a user types a character every 100 milliseconds, each character resets the timer. No search begins while typing continues. After the user pauses for 300 milliseconds, one search starts with the complete query. The intermediate queries are obsolete, so discarding them is desirable.

This makes debounce useful when the final state matters more than intermediate states:

  • Search suggestions and autocomplete
  • Validation after someone finishes editing a field
  • Filtering a list after the query changes
  • Autosaving a draft after edits pause
  • Recalculating a result after several related controls settle

The trade-off is that continuous activity can postpone a trailing debounce indefinitely. A preview debounced during a long drag may not update until the pointer stops. Some debounce utilities offer maxWait, which guarantees an invocation after a maximum elapsed time even if calls continue. When ongoing progress is important, however, throttling is often easier to reason about.

Debounce may also use leading-edge behavior: it invokes immediately on the first call, then suppresses or delays subsequent calls. Some implementations support both leading and trailing invocations. These options change the user experience, so specify the desired edge behavior rather than assuming all debounce functions work alike. Search commonly uses trailing behavior; an action that should respond immediately but resist repeated activation may use a leading policy.

Throttling: limit the rate

A throttle permits a callback to run at most once during each configured interval. Calls that arrive before the interval ends may be ignored, or combined into a trailing invocation with the latest arguments. Unlike a trailing debounce, a throttle can keep producing updates while events continue.

For example, a scroll handler that updates a reading-progress indicator could run at most once every 100 milliseconds. The browser may emit many scroll events within that time, but the indicator still receives regular updates without doing the same work for every event.

Throttling is a good fit when intermediate progress is useful but processing every intermediate value is unnecessary:

  • Scroll position indicators and scroll-driven interface updates
  • Pointer or drag previews
  • Resize feedback while a window is being resized
  • Progress reporting and sampling telemetry
  • Repeated actions that should have a minimum interval between executions

Throttle implementations also have edge policies. A leading throttle runs immediately when a call arrives after an idle interval. A trailing throttle runs at the end of the interval if calls arrived, usually using the latest call's arguments. A leading-and-trailing throttle gives an immediate response and a final update for the burst. A leading-only throttle can miss the final value; a trailing-only throttle delays the first response.

The timing difference

Consider calls at 0, 80, 160, and 200 milliseconds with a 200-millisecond wait. A trailing debounce restarts its timer on every call, so it runs around 400 milliseconds, after the quiet period begins. A leading throttle runs immediately at 0 and discards calls inside its interval. A leading-and-trailing throttle runs immediately and then around 200 milliseconds with the latest arguments received during that interval. The exact result depends on the chosen edge policy and timer scheduling.

The event name alone does not dictate the choice. Debounce a resize event if the application only needs final dimensions after resizing; throttle it if the user should see a chart preview update while resizing.

Simple implementations

This trailing debounce retains the most recent arguments and receiver (this):

function debounce(callback, wait = 300) {
  let timerId;

  return function (...args) {
    const context = this;
    clearTimeout(timerId);
    timerId = setTimeout(() => callback.apply(context, args), wait);
  };
}
Enter fullscreen mode Exit fullscreen mode

A basic leading throttle uses a timestamp and discards calls inside the interval:

function throttle(callback, wait = 100) {
  let lastCallTime = -Infinity;

  return function (...args) {
    const now = Date.now();
    if (now - lastCallTime < wait) return;

    lastCallTime = now;
    return callback.apply(this, args);
  };
}
Enter fullscreen mode Exit fullscreen mode

These snippets intentionally demonstrate different common policies: the debounce is trailing-only, while the throttle is leading-only. A production utility may need explicit validation, trailing behavior, and methods such as cancel() or flush(). The standalone companion examples in code/ explore those implementation details.

Create a wrapper once and reuse it. Every wrapper has its own timer or timestamp state; constructing a new one for each event defeats rate control. For example:

const search = debounce((query) => {
  console.log("Searching for:", query);
}, 300);

input.addEventListener("input", (event) => search(event.target.value));
Enter fullscreen mode Exit fullscreen mode

When a component is removed, unregister its event listener and cancel pending work if the utility supports cancellation. Otherwise, a delayed callback may try to update an interface that no longer exists.

For periodic feedback during ongoing activity, reuse a throttled wrapper instead. This example samples scroll position at most once per 100 milliseconds:

const updateScrollProgress = throttle(() => {
  const scrollableHeight =
    document.documentElement.scrollHeight - window.innerHeight;
  const progress =
    scrollableHeight > 0 ? window.scrollY / scrollableHeight : 0;

  console.log(`Reading progress: ${Math.round(progress * 100)}%`);
}, 100);

window.addEventListener("scroll", updateScrollProgress, { passive: true });
Enter fullscreen mode Exit fullscreen mode

This uses the leading-only throttle above, so it may not run once more with the final scroll position after a burst. Choose a trailing-capable implementation if that last update is required.

Timing, rendering, and asynchronous work

Neither technique makes a callback cheaper. It only reduces how often the callback starts. If a throttled callback blocks the main thread for 200 milliseconds, limiting it to one start per 100 milliseconds cannot make the page responsive. Measure the work itself and choose an interval that matches the interaction. Search may tolerate a few hundred milliseconds; visual feedback usually needs a shorter delay.

Timer delays are minimums, not exact deadlines. setTimeout makes a callback eligible after the requested delay, but a busy event loop or background-tab timer throttling can make it run later. For visual updates that should align with browser painting, requestAnimationFrame can coalesce work around frames. It is a rendering scheduler, not a substitute for waiting until activity stops.

Debouncing and throttling also govern when asynchronous work starts, not what happens after it starts. A debounced search request may finish after a newer request and overwrite current results. Use AbortController to cancel obsolete fetches where possible, or compare request identifiers and ignore stale responses. Promise-returning callbacks are not automatically awaited or serialized by either wrapper.

Finally, neither technique is suitable when every event must be preserved. Payments, audit events, or other must-not-lose operations need a queue or durable processing strategy. Debounce and throttle intentionally combine or discard calls; they do not guarantee delivery.

Choosing and testing

Ask what the interface should do during a burst. If it should wait until the user pauses and then act once on the latest state, choose debounce. If it should keep reporting progress but can skip some intermediate values, choose throttle. Then decide whether the first call should happen immediately, whether a final trailing call is needed, and how pending work should be canceled.

Test the timing policy rather than exact wall-clock precision. For debounce, verify that rapid calls produce one invocation with the latest arguments, separate bursts invoke separately, and cancellation suppresses pending work. For throttle, verify that invocations respect the interval and that leading and trailing behavior match the chosen policy. Fake timers make these tests deterministic. Also test receiver preservation if callbacks rely on this.

Both patterns are simple, but choosing the wrong one can make an interface either wasteful or unresponsive. Use debounce for settled state, throttle for periodic progress, and a queue when no event can be lost.

Quick comparison

Question Debounce Throttle
When does it run? After calls stop for the quiet period At most once per interval
What happens during a steady stream? A trailing call waits until activity pauses (unless maxWait is set) Calls keep producing updates at a controlled rate
What happens to intermediate calls? Usually replaced by the latest call Usually skipped or coalesced into a trailing call
Best for Search, autosave, validation, settled filters Scroll feedback, drag previews, resize feedback, sampling
Choose it when... Only the final state matters Progress during the interaction matters

Conclusion: Choose debounce for settled input and throttle for periodic feedback; use a queue if every event must be retained. If your project already uses Lodash, _.debounce and _.throttle provide established implementations, including options such as leading/trailing calls and cancellation (cancel(); debounce also supports flush() and maxWait). Prefer them over maintaining custom timing code when Lodash is already a dependency. For a small dependency-free utility, the standalone examples in code/ show the core behavior. Neither Lodash nor a custom wrapper cancels asynchronous work that has already started, so handle aborts and stale responses separately.

Top comments (0)