All topics
Asyncintermediate

Debouncing and Throttling

Two techniques for limiting how often a function runs in response to rapidly repeating events, like scrolling or typing.

Both limit how often a function runs during rapid events. They differ in which calls they keep.

Debouncing is an elevator door that keeps resetting its close timer as people approach. Throttling is a metered highway on-ramp letting one car through every few seconds no matter the queue.

Key Concepts

1
    debounce   wait until the events stop, then run once
    throttle   run at most once per interval, while events continue
2
Debounce: search-as-you-type.
    function debounce(fn, ms) {
      let t;
      return (...args) => {
        clearTimeout(t);
        t = setTimeout(() => fn(...args), ms);
      };
    }
3
    input.addEventListener('input', debounce(search, 300));
4
Every keystroke cancels the pending call and schedules a new one. Typing "javascript" fires one search, 300 ms after the last letter — not ten.
5
Throttle: scroll position.
    function throttle(fn, ms) {
      let last = 0;
      return (...args) => {
        const now = Date.now();
        if (now - last >= ms) {
          last = now;
          fn(...args);
        }
      };
    }
6
    window.addEventListener('scroll', throttle(updateHeader, 100));
7
Scrolling fires hundreds of events a second. Throttling runs the handler ten times a second, evenly, while scrolling continues.
8
Which to use is decided by one question: do you need updates during the activity?
    search input        no  -> debounce; only the final query matters
    window resize       no  -> debounce; act when resizing stops
    scroll position     yes -> throttle; the header must move as
                               you scroll
    button double-click yes -> throttle; act on the first, ignore
                               the rest
    autosave a draft    no  -> debounce
9
The leading-edge variant. Plain debounce waits before the first call, which feels unresponsive on a button. A leading-edge debounce runs immediately and then ignores further calls until things settle:
10
    submitButton.addEventListener('click', debounce(submit, 1000, { leading: true }));
11
The mistake that makes both do nothing.
    // wrong -- a new debounced function on every render
    <input onChange={debounce(search, 300)} />
12
Each render creates a fresh timer, so nothing is ever cancelled. The debounced function must be created once — useMemo, useCallback, or outside the component.
useMemouseCallback
13
And cancellation. A pending debounced call after a component unmounts still fires. Real implementations expose .cancel(), and calling it on teardown is what prevents the update-after-unmount warning.
.cancel()
14
What the interviewer is probing.1. "What is the difference?" Probing: the two behaviours. Stalls: "Both delay things." Moves up: debounce waits for a quiet period and runs once at the end; throttle runs at most once per interval while events continue.
15
2. "Which suits a search box, and which a scroll handler?" Probing: applying it. Stalls: "Either." Moves up: debounce for search, so one request fires when typing stops; throttle for scroll, so updates keep happening at a bounded rate.
16
3. "What breaks if you create the debounced function inside a render?" Probing: the identity problem. Stalls: "Nothing." Moves up: a new function each render means the timer is never shared, so it never debounces — it must be created once.
17
4. "What is the leading edge option for?" Probing: responsiveness. Stalls: "Nothing." *Moves up:* running immediately on the first event and then suppressing, so the UI feels instant while still being limited.