Performance Patterns: Debouncing & Throttling
Some events fire dozens of times a second — every keystroke, every scroll. Debouncing and throttling tame that flood so your code (and any API) is not overwhelmed. A real-world must and a common interview question.
What you will learn
- Explain why rapid-fire events are a problem
- Use debouncing to wait until activity stops
- Use throttling to run at most once per interval
The problem: events that fire too often
Some events fire extremely often. The input event fires on every single keystroke; the scroll and resize events can fire dozens of times a second. If your handler does expensive work each time — calling an API, recalculating a layout — you flood the system, waste bandwidth, and make the page feel sluggish.
Two patterns fix this, and both are built on a timer plus a closure (the closure remembers the timer between calls):
- Debouncing — wait until the activity stops for a moment, then run once. Good for: search-as-you-type, validating a field after the user pauses.
- Throttling — run at most once per fixed interval, no matter how many times the event fires. Good for: scroll and resize handlers, where you want steady updates but not hundreds per second.
Debouncing — wait for the pause
The mental model: every time the event fires, you cancel the previous pending action and start the timer again. The action only actually runs once the events stop long enough for the timer to finish. Imagine a lift that waits a few seconds after the last person steps in before closing the doors.
<input id="search" placeholder="Type to search...">
<p id="status">Waiting for you to type…</p>
<script>
const status = document.getElementById("status");
// debounce: returns a wrapped function that only runs after a quiet gap
function debounce(fn, delay) {
let timer; // remembered via closure
return function (...args) {
clearTimeout(timer); // cancel the previous pending run
timer = setTimeout(() => fn(...args), delay); // restart the wait
};
}
function search(text) {
status.textContent = text
? "Searching for: " + text
: "Waiting for you to type…";
}
// Only search 500ms AFTER the user stops typing
document.getElementById("search")
.addEventListener("input", debounce(e => search(e.target.value), 500));
</script>Here is the flow as you type:
debouncewraps your function and keeps a privatetimerin a closure.- Every keystroke fires
input, which calls the wrapped function. clearTimeout(timer)cancels any run that was scheduled but has not happened yet.setTimeout(..., 500)schedules the realsearchto run in 500ms — but only if no new keystroke cancels it first.- So while you keep typing, the action keeps getting pushed back; it finally runs once, 500ms after your last keystroke.
Note: Output: Type "lap" quickly → nothing happens yet. Stop typing for half a second → "Searching for: lap" appears once. (Without debounce, it would have searched three times — once per letter.)
Throttling — a steady maximum rate
Throttling is different: instead of waiting for a pause, it lets the function run immediately, then ignores further calls until a set interval has passed. Picture a turnstile that lets one person through, then locks for two seconds no matter how many push on it. This keeps a steady, predictable rate — ideal for scroll or resize.
<button id="tick">Click me fast!</button>
<p id="count">Allowed runs: 0</p>
<script>
let runs = 0;
const out = document.getElementById("count");
function throttle(fn, interval) {
let ready = true; // can we run right now?
return function (...args) {
if (!ready) return; // still cooling down → ignore
ready = false;
fn(...args); // run immediately
setTimeout(() => { ready = true; }, interval); // unlock after interval
};
}
const handleClick = throttle(() => {
runs++;
out.textContent = "Allowed runs: " + runs;
}, 1000);
document.getElementById("tick").addEventListener("click", handleClick);
</script>The wrapped function keeps a ready flag in its closure. The first click runs immediately and sets ready to false; any clicks during the next second are simply ignored. After 1000ms a timer flips ready back to true, allowing the next run. So however furiously you click, the counter rises at most once per second — a guaranteed maximum rate.
Note: Output: Click the button 10 times in one second → "Allowed runs: 1". Wait a second and click again → "Allowed runs: 2". (Throttle enforces a steady rate; the extra clicks are dropped.)
Which one do I use?
| Pattern | When it runs | Best for |
|---|---|---|
| Debounce | Once, after the events STOP for a delay | Search-as-you-type, autosave, validating after a pause |
| Throttle | At most once per fixed interval, while events continue | Scroll, resize, drag, rapid button clicks |
Tip: In real projects you will often use a tiny library (like lodash’s debounce/throttle) instead of hand-writing these — but interviewers frequently ask you to implement them from scratch, exactly as shown here. Knowing the timer-plus-closure mechanism is what they are testing.
Q. You want to call a search API only after the user stops typing for a moment. Which pattern fits?
✍️ Practice
- Add a debounced handler to a text box that logs the value only 400ms after typing stops.
- Throttle a scroll handler so it updates a counter at most once every 500ms.
🏠 Homework
- Build a search box that debounces the input and, after the pause, fetches matching results from a public API (e.g. jsonplaceholder users filtered by the typed text).