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). It returns a function that, every time it's called, restarts a timer. When wait ms pass with no new call, it calls fn once, with the this and arguments of the most recent call.
As long as calls keep coming less than wait ms apart, fn doesn't run at all. The returned function also has a cancel() method that drops a pending call.
const save = debounce((text) => console.log("saving", text), 300);
save("h");
save("he");
save("hel");
// 300 ms after the last call: logs "saving hel", once
expect(typeof debounce(() => {}, 100)).toBe("function");
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100);
debounced();
clock.tick(50);
debounced();
clock.tick(99);
expect(fn).toHaveBeenCalledTimes(0);
clock.tick(1);
expect(fn).toHaveBeenCalledTimes(1);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100);
for (let i = 0; i < 10; i++) {
debounced();
clock.tick(90);
}
expect(fn).toHaveBeenCalledTimes(0);
clock.tick(10);
expect(fn).toHaveBeenCalledTimes(1);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100);
debounced("a");
debounced("b", 2);
clock.tick(100);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenLastCalledWith("b", 2);
const clock = useFakeTimers();
const fn = spy();
const box = { save: debounce(fn, 10) };
box.save();
clock.tick(10);
expect(fn.calls[0].this).toBe(box);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100);
debounced(1);
clock.tick(100);
debounced(2);
clock.tick(100);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenLastCalledWith(2);
const clock = useFakeTimers();
const fn = spy();
const debounced = debounce(fn, 100);
debounced();
debounced.cancel();
clock.tick(200);
expect(fn).not.toHaveBeenCalled();
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. 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.