How do you implement debouncing and throttling in JavaScript?
Advertisement
🧩 Scenario
Architecture Walkthrough
Debounce: Wait for the Noise to Stop
Debounce delays the execution of a function until after a specified period of inactivity. Every time the debounced function is called, any pending timer is cleared and a new one is started. The wrapped function only executes when the timer completes without being interrupted. If events arrive faster than the delay, the function is never called until they stop.
The implementation uses a closure to maintain the timeout reference across calls. clearTimeout cancels the previous timer, and setTimeout schedules a new one. This pattern is ideal for any event where you only care about the final state after a burst of activity ends: search input, window resize completion, form validation after typing stops.
Throttle: Limit the Rate
Throttle ensures a function is called at most once per interval, regardless of how many times it is invoked. The first call in each interval always executes immediately. Subsequent calls during the same interval are ignored. After the interval passes, the flag resets and the next call can execute.
The implementation uses a boolean flag to track whether the function is currently in its quiet period. The flag is set on the first call, and a timer resets it after the interval. Throttle is appropriate when you want regular, periodic execution during continuous activity: scroll position updates, rate-limited analytics, and canvas mouse-tracking all benefit from throttle rather than debounce.
Closures Power Both Patterns
Both debounce and throttle are higher-order functions that return a new function. The returned function closes over the timeout or inThrottle variable from the outer scope. This closure is what allows the timer state to persist between separate calls to the returned function without using global variables.
In React, this means the debounced or throttled function must be stable across renders. Creating it inside the component body without useCallback produces a new function on every render, discarding the timer state. The useCallback hook (or a useRef to store the function) is necessary to maintain the closure correctly.
Key Code Explained
// Debounce: resets timer on every call
function debounce(fn, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
}
// Throttle: executes at most once per interval
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Debounce in React: memoize with useCallback to preserve closure
const handleSearch = useCallback(
debounce((query) => {
fetchResults(query);
}, 300),
[], // stable reference across renders
);
// Cleanup in useEffect to avoid memory leaks
useEffect(() => {
const debouncedResize = debounce(handleResize, 200);
window.addEventListener('resize', debouncedResize);
return () => {
window.removeEventListener('resize', debouncedResize);
// Also clear any pending timer if debounce exposes a cancel method
};
}, []);
The React usage pattern is the most important real-world detail. Without useCallback, every render creates a new debounced function with a fresh timer, so the accumulated delay resets every time the component re-renders. Wrapping with useCallback and an empty dependency array ensures the same debounced function instance is reused.
Tradeoffs
| Pattern | Fires when | Risk of missing events | Best for |
|---|---|---|---|
| Debounce (trailing) | After activity stops | Yes, intermediate events are lost | Search input, form validation, resize completion |
| Debounce (leading) | On first call, then after quiet period | Yes, intermediate events are lost | Preventing double-submit on a button |
| Throttle | At most once per interval | Yes, but at a predictable rate | Scroll, mousemove, real-time analytics |
| No rate limiting | On every event | No | Simple, infrequent events only |
What Interviewers Actually Check
- Whether you can implement both from scratch without reaching for a library
- Whether you understand the closure mechanics that make the timer state persist
- Whether you can explain when each pattern is appropriate and give concrete examples
- Whether you know the React-specific pitfall of recreating debounced functions on every render
- Whether you can describe leading vs trailing invocation
Follow-Up Questions
- How would you add a
cancelmethod to a debounce implementation that clears the pending timer? - How does
requestAnimationFramerelate to throttling for animation and rendering use cases? - If both debounce and throttle cause you to miss events, what pattern would you use when you need the last event's value but want to rate-limit the frequency?
- How would you test a debounced function in a unit test without actually waiting for the timer?
- Lodash provides
_.debounceand_.throttlewith leading, trailing, and maxWait options. When would you needmaxWait?
Common Candidate Mistakes
- Using debounce for scroll events and losing the intermediate scroll positions that the handler needed to process
- Using throttle for search input and sending unnecessary API calls every interval instead of waiting for the user to pause
- Not using
useCallbackin React for debounced handlers, causing timer state to reset on every render - Creating the debounced function inside a
useEffector callback where it is recreated on every invocation - Not cleaning up the event listener and timer on component unmount
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you write a debounce implementation from scratch and explain how
clearTimeoutresets the delay? - Can you write a throttle implementation from scratch and explain the flag and timer mechanism?
- Can you explain the difference between leading and trailing invocation for both patterns?
- Can you describe how to properly use a debounced function in React with
useCallbackand cleanup? - Can you name three real event types that benefit from debounce and three that benefit from throttle?
Summary
Debounce and throttle are rate-limiting patterns for high-frequency events. Debounce delays execution until activity has paused for a specified duration: every new event resets the timer, and the function only runs after quiet. Throttle limits execution to at most once per interval: the first call runs, then the function is blocked until the interval resets.
Both patterns are higher-order functions that use closures to maintain timer state across calls. In React, the debounced or throttled function must be stable across renders, which requires useCallback or a ref to hold the instance. Creating it inside the render body defeats the purpose by discarding the accumulated timer state on every render.
Choosing between them depends on the event type: debounce is for situations where only the final state matters (typing is done, resizing is complete), and throttle is for situations where periodic updates during activity are valuable (scroll position, mouse coordinates, live analytics).
When should I use debounce vs throttle?
Use debounce for actions that should fire once after activity stops, like search input. Use throttle for continuous events that should fire at a controlled rate, like scroll or resize.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement