πAdvancedAdvanced JavaScript
Debouncing and Throttling
Two essential techniques for controlling how often a function runs in response to rapid events
Overview
When a user types in a search box, scrolls a page, or resizes a window, the browser can fire hundreds of events per second. Without control, each event would trigger your handler β an API request, a DOM update, or an expensive calculation β hundreds of times per second. Debouncing and throttling are two complementary patterns that let you take back control, ensuring expensive work runs only as often as it needs to.
Debouncing delays a function's execution until after a burst of events has stopped. Picture a search-as-you-type field: you don't want to fire an API request for every single keystroke, because the user might type five more letters before you get a useful response. A debounced handler waits until the user has stopped typing for, say, 300ms, then fires exactly once with the final value. If the user types again within that 300ms window, the timer resets. Debounce is ideal for any situation where you only care about the final, settled value: search inputs, save-as-you-type, form validation, and window resize completions.
Throttling guarantees a function fires at most once per interval, regardless of how many events arrive. If a scroll event fires 60 times per second but you only need to recalculate a sticky header's position every 100ms, throttle caps you at 10 calls per second and ignores the rest. Unlike debounce β which fires only after events stop β throttle fires continuously but at a controlled rate. It is ideal for ongoing, progress-tracking events: scroll position, mouse movement, real-time game input, and live data streaming.
Both patterns are implemented using closures, `setTimeout`, and `clearTimeout`. Understanding how to write them from scratch is more valuable than memorising library APIs: you learn to tune the timing, implement leading- or trailing-edge variants, and handle edge cases like cleanup on component unmount. Libraries like Lodash provide polished versions β but when you need precise control or a lighter bundle, rolling your own takes less than ten lines.
Key Points
- β’Debounce fires once after a burst of events has settled β the timer resets every time a new event arrives
- β’Throttle fires at most once per interval β it does not reset on new events, it simply skips them
- β’Use debounce for "fire after the user finishes": search inputs, auto-save, form validation, resize completion
- β’Use throttle for "fire at a steady rate": scroll handlers, mouse-move effects, live data updates
- β’Both are closures that capture a timer reference; debounce uses `clearTimeout` + `setTimeout`, throttle tracks last-call time
- β’Always cancel pending debounced calls on component unmount to avoid state updates on unmounted components
- β’`_.debounce` and `_.throttle` from Lodash are production-ready with cancel/flush methods; write your own when bundle size matters
Examples
Basic debounce implementation
function debounce(fn, wait) {
let timer;
return function debounced(...args) {
clearTimeout(timer); // cancel any pending call
timer = setTimeout(() => {
fn.apply(this, args); // fire after wait ms of silence
}, wait);
};
}
// Usage
const log = debounce((val) => console.log('Search:', val), 300);
// Simulate rapid keystrokes 50ms apart
['J','Ja','Jav','Java','JavaS'].forEach((v, i) => {
setTimeout(() => log(v), i * 50);
});
// Only 'JavaS' is logged β after 300ms of silenceDebounce applied to a search input
const input = document.getElementById('search');
function fetchResults(query) {
console.log('Fetching:', query); // represents an API call
}
const debouncedFetch = debounce(fetchResults, 300);
input.addEventListener('input', (e) => {
debouncedFetch(e.target.value);
});
// fetchResults is called once per typing pause, not per keystrokeBasic throttle implementation
function throttle(fn, interval) {
let lastCalled = 0;
return function throttled(...args) {
const now = Date.now();
if (now - lastCalled >= interval) {
lastCalled = now;
fn.apply(this, args);
}
// calls within the interval are silently dropped
};
}
const log = throttle((pos) => console.log('Scroll:', pos), 200);
// Simulate 10 scroll events 50ms apart (total 500ms)
for (let i = 0; i < 10; i++) {
setTimeout(() => log(i * 100), i * 50);
}
// Only ~3 calls fire (at 0ms, 200ms, 400ms) β not 10Throttle applied to a scroll handler
const updateHeader = throttle(() => {
const scrolled = window.scrollY > 100;
document.getElementById('header').classList.toggle('sticky', scrolled);
}, 100); // at most 10 times per second
window.addEventListener('scroll', updateHeader);
// Without throttle: fires ~60x/sec on smooth scroll
// With throttle: fires 10x/sec, same visual resultDebounce with cancel β essential for React/cleanup
function debounce(fn, wait) {
let timer;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
}
debounced.cancel = () => clearTimeout(timer); // cleanup method
return debounced;
}
// In a React-like component:
// const debouncedSave = debounce(saveToServer, 500);
// input.addEventListener('input', debouncedSave);
// onComponentUnmount: debouncedSave.cancel(); // prevent stale updateLodash's debounce includes cancel() and flush() methods by default.
Common Mistakes
- Creating a new debounce/throttle wrapper inside a React component body or event loop β a new wrapper resets the internal timer on every render, defeating the purpose; wrap with useRef or useMemo
- Confusing which pattern to use: debounce on a scroll event means the handler only fires after scrolling stops β apply throttle instead for continuous feedback during scrolling
- Forgetting to cancel pending debounce on component unmount β causes `setState` calls on unmounted components, leading to memory leaks and React warnings
- Setting the wait interval too long (UI feels sluggish) or too short (no benefit over no throttling) β 150β300ms for debounce and 100β200ms for throttle are common starting points
Note
When in doubt between debounce and throttle, ask: "Do I want to know the outcome after everything settles (debounce) or do I want regular updates while it's happening (throttle)?" If the answer is the former, debounce. If the latter, throttle.
Exercises
Write JavaScript code to solve each exercise. Run it and check your output.
Loading editorβ¦
Completed: 0 / 6 exercises
Knowledge Check
Question 1 of 4