Debounce
Waiting until rapid events pause, preserving the caller's this value when a debounced method still needs its object context.
Some browser events fire far more often than the user can see. Typing can trigger dozens of callbacks per second. Debounce waits for the noise to stop, then runs the handler once with the latest arguments.
Debounce is a timer that keeps getting reset. This version uses fn.apply(this, args) so the wrapped function receives the same this value as the debounced wrapper. That matters when the function is an object method.
function debounce(fn, delay = 300) {
let timerId;
return function debounced(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}Here the debounced function is called as searchBox.onInput(). JavaScript sets this to searchBox for that call. apply preserves that object context until the timer runs, so this.query still points at the right object.
const searchBox = {
query: "",
onInput(value) {
this.query = value;
console.log("search for", this.query);
},
};
searchBox.onInput = debounce(searchBox.onInput, 400);
searchBox.onInput("j");
searchBox.onInput("js");
searchBox.onInput("javascript");
// After 400 ms:
// search for javascriptWithout this preservation, method-style calls lose their object context inside the delayed callback. The broken version below calls fn(...args), so onInput no longer runs as a method of searchBox.
function brokenDebounce(fn, delay = 300) {
let timerId;
return function debounced(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn(...args);
}, delay);
};
}
const searchBox = {
query: "",
onInput(value) {
this.query = value;
console.log("search for", this.query);
},
};
searchBox.onInput = brokenDebounce(searchBox.onInput, 400);
searchBox.onInput("javascript");
// TypeError in strict mode because this is undefined.
// In sloppy mode, this may point at the global object instead.If the callback does not use this, the smaller arrow-function version is fine. DOM event handlers often only need the event argument, so there is no object context to preserve.
<input id="search" type="search" placeholder="Search" />function debounceValue(fn, delay = 300) {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
}
const searchInput = document.querySelector("#search");
const handleSearch = debounceValue((event) => {
console.log("search for", event.target.value);
}, 400);
searchInput.addEventListener("input", handleSearch);In production
Debounce is a good default for autosave and autocomplete, but it can lose the last action if the user closes the tab before the timer fires. Never debounce a critical write without a fallback path, such as saving on submit, blur, visibility change, or page unload where the platform allows it.
Debounce waits for silence, and apply keeps method calls attached to the object they came from.