What is the difference between debounce and throttle? Implement both.
Debounce waits until calls stop for a set time and then runs the function once, like search-as-you-type. Throttle runs the function at most once per time window no matter how often it is called, like handling scroll events. Both are built with a closure that holds a timer or a timestamp.
Explain it simply
Picture a lift. Debounce is the lift door: every time someone new walks in, the door waits again. It only closes once nobody has entered for a few seconds. Throttle is a metro train that leaves every 5 minutes. However many people arrive, it departs on a fixed rhythm, at most once per interval.
In code, some events fire very fast. Typing fires on every key, scrolling and resizing fire dozens of times per second. Running heavy work, like an API call, on every single event wastes network and makes the page lag.
- Debounce: "Run this only after the user has stopped for 300 ms." Typing "biryani" makes 1 API call instead of 7.
- Throttle: "Run this at most once every 200 ms." While scrolling, the handler runs regularly but not on every pixel.
Line by line
function debounce(fn, delay) {
let timerId; // lives in the closure between calls
return function (...args) {
clearTimeout(timerId); // cancel the previous waiting call
timerId = setTimeout(() => {
fn.apply(this, args); // run with the latest arguments
}, delay);
};
}
function throttle(fn, interval) {
let lastRun = 0; // time of the last real call
return function (...args) {
const now = Date.now();
if (now - lastRun >= interval) {
lastRun = now;
fn.apply(this, args); // run now; ignore calls until interval passes
}
};
}
// Usage
const search = debounce((text) => console.log("Searching for", text), 300);
search("b");
search("bi");
search("biryani");
// After 300 ms of silence, only one line prints:
// Searching for biryani
const onScroll = throttle(() => console.log("scroll handled"), 200);
window.addEventListener("scroll", onScroll);timerIdandlastRunlive in a closure, so each debounced or throttled function keeps its own state.clearTimeout(timerId)is the heart of debounce: each new call resets the countdown.fn.apply(this, args)passes along the originalthisand the latest arguments, so the wrapper works for methods too. The inner arrow function inheritsthisfrom the wrapper.- The throttle above is a "leading edge" version: the first call runs immediately and calls inside the window are dropped.
How it works
| Debounce | Throttle | |
|---|---|---|
| When it runs | After calls stop for delay ms | At most once per interval ms |
| During a continuous burst | Does not run until the burst ends | Runs regularly |
| Typical use | Search box, auto-save, resize end, form validation | Scroll, mouse move, drag, button spam |
Complexity
Each call to a debounced or throttled function does O(1) work: one comparison or one timer reset. Memory is O(1) per wrapped function, just the timer id or timestamp and the latest arguments.
Real-world use
- Swiggy or Zomato search suggestions use debounce so the server is not hit on every key press.
- Infinite scroll checks "am I near the bottom?" with throttle.
- A "Pay now" button can use a throttle-like guard so double taps do not trigger two payment requests (the server should still enforce this).
Common mistakes
- Creating a new debounced function on every render or every event. The closure state is lost, so it never debounces. Create it once.
- Using arrow functions for the returned wrapper and then losing the caller's
this. - Choosing debounce for scroll tracking, so nothing happens until the user stops scrolling.
- Forgetting to cancel pending timers when a component unmounts.
What to say in the interview
"Both limit how often a function runs. Debounce delays the call until events have stopped for a given time; every new event resets a timer, so a burst produces one call at the end. That suits search-as-you-type. Throttle guarantees the function runs at most once per interval during a burst, which suits scroll or mousemove handlers. I implement debounce with a closure holding a setTimeout id that I clear on every call, and throttle with a closure holding the last run timestamp. Both are O(1) per call."
Follow-up questions
- How would you add a
cancelmethod to your debounce? - What is the difference between leading-edge and trailing-edge throttle?
- How would you use debounce inside a React component without recreating it on each render?
Go deeper
Read these once the basics above feel easy.
Production versions such as Lodash's add options: leading (run on the first call too), trailing (run once more at the end of the window with the last arguments) and maxWait (force a run if calls never stop). A throttle with a trailing call is useful for scroll position, because otherwise the final position might be skipped. Adding cancel is simple: return the wrapper with a cancel = () => clearTimeout(timerId) property attached.