Some events fire many times in quick succession: input fires every time the text in a box changes, and resize fires over and over while a window is resized. Often only the end result matters. Debouncing a function makes it wait for a pause. However often it's called, it runs only after the calls have stopped for a set time, called the wait, and then it runs once, with the arguments of the most recent call. Every new call during the wait starts the wait over. Calls that come closer together than the wait form a burst.
Picture a lift that waits for people to stop arriving. Each time someone steps in, the doors stay open a few seconds longer. The lift leaves only once nobody has stepped in for that long, and it makes one trip, however many people got in.
Say search(text) fetches results for a search box. The box's input handler doesn't call search directly: it calls a debounced version of it, with a wait of 300 ms, passing the text. The user types cat quickly, pauses, then adds an s:
| Time | Debounced call | What happens |
|---|---|---|
| 0 ms | "c" | A burst starts. If no other call comes, search will run at 300 ms. |
| 100 ms | "ca" | A call during the wait starts it over: now search will run at 400 ms. |
| 250 ms | "cat" | The wait starts over again: 550 ms. |
| 550 ms | None | 300 ms have passed without a call, so the burst ends and search("cat") runs. |
| 900 ms | "cats" | A new burst starts: search will run at 1200 ms. |
| 1200 ms | None | search("cats") runs. |
Four calls, two searches, each for the text the user had stopped at. No request is ever sent for c or ca. The cost is a delay: search runs 300 ms after the last key press, and as long as calls keep coming less than 300 ms apart, it doesn't run at all.
Running at the end of a burst, as here, is called running on the trailing edge. A debounce can instead, or also, run on the leading edge: right away, on the call that starts a burst. Lodash's _.debounce has leading and trailing options for this, and with both on, a burst of one call runs once, not twice. Its maxWait option is the longest a run can be put off: once a burst has gone on that long, the function runs then, with the latest arguments, without waiting for the pause. That way an autosave still saves now and then while someone types without stopping. Here's the first burst above under each setting:
| Setting | The function runs | With |
|---|---|---|
| Trailing edge (the usual default) | At 550 ms | "cat" |
| Leading edge only | At 0 ms | "c" |
| Both edges | At 0 ms and at 550 ms | "c", then "cat" |
| Both edges, if only the call at 0 ms is made | At 0 ms only | "c" |
Trailing edge, with a maxWait of 400 ms | At 400 ms, not 550 ms | "cat" |
Debouncing is often confused with throttling. A throttle runs the function at most once per interval while the calls keep coming; a debounce waits until they stop. Debounce when only the final value matters: search as you type, autosave, checking a field once the user stops typing, or redoing a layout once a window stops resizing. Debounce helpers often also have cancel(), which drops a pending run, and flush(), which runs it right away; Lodash's _.debounce has both.
Write debounce(fn, wait, { leading, trailing, maxWait }). Calls come in bursts. A burst starts with a call made when no burst is going on, and every call in it restarts a wait ms timer. The burst ends when that timer fires, that is, after wait ms without calls.
leading (default false) calls fn right away, during the call that starts a burst, with that call's this and arguments.
trailing (default true) calls fn when the burst ends, with the this and arguments of the latest call, but only if a call came after fn last ran. So with both on, a burst of one call runs fn once, and a longer burst runs it at both edges.
maxWait (optional, never less than wait) is the longest a burst can last. When a burst has lasted maxWait ms, fn runs with the latest call's this and arguments (if a call came after fn last ran), even though calls keep coming, and the burst is over: the next call starts a new one.
Add a cancel() method that drops any pending call and ends the burst.
const save = debounce(saveDraft, 1000, { maxWait: 5000 });
// saves 1 s after typing stops, and at least every 5 s while typing goes on
// wait = 100, calls at 0, 50, and 100 ms:
// {} -> fn runs at 200
// { leading: true, trailing: false } -> fn runs at 0
// { leading: true } -> fn runs at 0 and at 200
// { maxWait: 120 } -> fn runs at 120, with the call from 100
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100);
debounced(1);
clock.tick(50);
debounced(2);
clock.tick(99);
expect(fn).toHaveBeenCalledTimes(0);
clock.tick(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenLastCalledWith(2);
useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100, { leading: true });
debounced(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenLastCalledWith(1);
debounced(2);
expect(fn).toHaveBeenCalledTimes(1);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100, { leading: true, trailing: false });
debounced(1);
clock.tick(60);
debounced(2);
clock.tick(60);
debounced(3);
clock.tick(1000);
expect(fn.calls.map((call) => call.args[0])).toEqual([1]);
debounced(4);
expect(fn.calls.map((call) => call.args[0])).toEqual([1, 4]);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100, { leading: true, trailing: true });
debounced(1);
clock.tick(1000);
expect(fn).toHaveBeenCalledTimes(1);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100, { leading: true, trailing: true });
debounced(1);
clock.tick(50);
debounced(2);
clock.tick(99);
expect(fn.calls.map((call) => call.args[0])).toEqual([1]);
clock.tick(1);
expect(fn.calls.map((call) => call.args[0])).toEqual([1, 2]);
const clock = useFakeTimers();
const start = clock.now();
const runs = [];
const debounced = debounce((value) => runs.push(value + " at " + (clock.now() - start)), 100, { maxWait: 200 });
for (let i = 0; i < 10; i++) {
debounced(i); // a call every 50 ms, from 0 to 450
clock.tick(50);
}
clock.tick(1000);
expect(runs).toEqual(["3 at 200", "7 at 400", "9 at 550"]);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100);
const a = { debounced };
const b = { debounced };
a.debounced(1);
b.debounced(2, 3);
clock.tick(100);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn.calls[0].this).toBe(b);
expect(fn).toHaveBeenLastCalledWith(2, 3);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100, { leading: true });
debounced(1);
debounced(2);
debounced.cancel();
clock.tick(1000);
expect(fn.calls.map((call) => call.args[0])).toEqual([1]);
debounced(3);
expect(fn.calls.map((call) => call.args[0])).toEqual([1, 3]);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Debounce with leading, trailing, and maxWait. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.