Implement Debounce
This is one of the most common coding questions in senior frontend interviews. The implementation is straightforward — the differentiation is in edge cases and real-world application.
Core Implementation
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}How it works: Every call resets the timer. The function only executes after the caller stops calling for delay ms.
Senior-Level Implementation
Production debounce needs more:
function debounce(fn, delay, options = {}) {
let timeoutId;
let lastArgs;
let lastThis;
let result;
const { leading = false, trailing = true, maxWait } = options;
let lastCallTime;
let lastInvokeTime = 0;
function invoke() {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = Date.now();
result = fn.apply(thisArg, args);
return result;
}
function startTimer(pendingFn, wait) {
timeoutId = setTimeout(pendingFn, wait);
}
function trailingEdge() {
timeoutId = undefined;
if (trailing && lastArgs) invoke();
lastArgs = lastThis = undefined;
}
function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timeoutId === undefined && leading) {
lastInvokeTime = time;
startTimer(trailingEdge, delay);
return invoke();
}
if (maxWait !== undefined) {
startTimer(trailingEdge, delay);
return invoke();
}
}
if (timeoutId === undefined) {
startTimer(trailingEdge, delay);
}
return result;
}
function shouldInvoke(time) {
const timeSinceLastCall = time - (lastCallTime ?? 0);
const timeSinceLastInvoke = time - lastInvokeTime;
return (
lastCallTime === undefined ||
timeSinceLastCall >= delay ||
(maxWait !== undefined && timeSinceLastInvoke >= maxWait)
);
}
debounced.cancel = function () {
clearTimeout(timeoutId);
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timeoutId = undefined;
};
debounced.flush = function () {
if (timeoutId !== undefined) trailingEdge();
return result;
};
return debounced;
}Key Options to Discuss
| Option | Purpose |
|---|---|
leading | Fire on the first call, not the last |
trailing | Fire after the delay (default behavior) |
maxWait | Guarantee execution at least every N ms |
cancel | Cancel pending invocations |
flush | Execute pending invocation immediately |
Real-World Usage
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const search = useMemo(
() => debounce(async (q) => {
const data = await fetchResults(q);
setResults(data);
}, 300),
[]
);
useEffect(() => () => search.cancel(), [search]);
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
search(e.target.value);
}}
/>
);
}Debounce vs. Throttle
- Debounce: Wait until activity stops, then execute once
- Throttle: Execute at most once per interval, regardless of activity
Use debounce for: search input, form validation, resize handlers. Use throttle for: scroll handlers, mouse move, API polling.
The interview signal isn't whether you can write debounce — it's whether you know when to use it, when to use throttle, and when neither is appropriate.