Difficulty: Advanced
Debouncing and throttling are rate-limiting techniques that control how frequently a function executes in response to rapid, repeated events like scrolling, resizing, typing, or button clicks. They are essential for performance optimization and are among the most commonly asked JavaScript interview questions. Understanding both concepts and being able to implement them from scratch is a core expectation.
Debouncing delays the execution of a function until a specified time has passed since the last invocation. If the function is called again before the delay expires, the timer resets. This means the function only executes once the rapid calls have stopped. The classic example is a search input: instead of making an API call on every keystroke, you debounce the search function so it only fires after the user stops typing for, say, 300 milliseconds. The implementation uses setTimeout to schedule execution and clearTimeout to cancel the previous timer whenever a new call arrives.
Throttling limits the execution of a function to at most once per specified time interval. Unlike debouncing, throttling guarantees regular execution during sustained activity. For example, throttling a scroll handler to 100ms ensures it fires at most 10 times per second, regardless of how fast the user scrolls. The implementation tracks the last execution time and only allows the function to run if enough time has elapsed since the last call.
Both techniques come in leading-edge and trailing-edge variants. Trailing edge (default) executes after the delay period ends. Leading edge executes immediately on the first call and then waits. Some implementations support both, giving immediate feedback followed by delayed execution. Libraries like Lodash provide configurable debounce and throttle with leading, trailing, and cancel options.
requestAnimationFrame (rAF) can serve as an alternative to throttle for visual updates. rAF schedules a callback to run before the next browser repaint, which happens roughly 60 times per second (every ~16ms). This is ideal for animations and scroll-driven visual effects because it naturally synchronizes with the browser's rendering cycle. Unlike a fixed-interval throttle, rAF adapts to the device's refresh rate and pauses when the tab is not visible.
Real-world applications are everywhere: debounce for search-as-you-type, auto-save after editing stops, and form validation after the user finishes typing. Throttle for scroll position tracking, infinite scroll loading, window resize handlers, and mousemove-based interactions. rAF for parallax scrolling, progress bars, and canvas animations. Choosing between debounce and throttle depends on whether you want to wait until activity stops (debounce) or ensure regular updates during activity (throttle).
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
// Clear previous timer (resets the delay)
clearTimeout(timeoutId);
// Set new timer
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage: search input
function searchAPI(query) {
console.log(`API call for: "${query}"`);
}
const debouncedSearch = debounce(searchAPI, 300);
// Simulating rapid typing: h, he, hel, hell, hello
debouncedSearch('h'); // timer set
debouncedSearch('he'); // timer reset
debouncedSearch('hel'); // timer reset
debouncedSearch('hell'); // timer reset
debouncedSearch('hello'); // timer reset - only this one fires after 300ms
// After 300ms: API call for: "hello"
Each call clears the previous timeout and sets a new one. Only the last call's timer completes because all earlier timers are cancelled by subsequent calls. The function executes once the rapid calls stop.
function debounce(fn, delay, { leading = false } = {}) {
let timeoutId;
let isLeadingInvoked = false;
function debounced(...args) {
// Leading edge: execute immediately on first call
if (leading && !isLeadingInvoked) {
fn.apply(this, args);
isLeadingInvoked = true;
}
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
// Trailing edge: execute after delay
if (!leading) {
fn.apply(this, args);
}
isLeadingInvoked = false;
}, delay);
}
debounced.cancel = () => {
clearTimeout(timeoutId);
isLeadingInvoked = false;
};
return debounced;
}
// Leading: fires immediately, then ignores until delay passes
const saveNow = debounce(
(data) => console.log('Saved:', data),
1000,
{ leading: true }
);
saveNow('draft 1'); // fires immediately
saveNow('draft 2'); // ignored (within 1000ms)
saveNow('draft 3'); // ignored (within 1000ms)
// After 1000ms of silence, the next call will fire immediately again
// Cancel: abort pending execution
const debouncedFn = debounce((x) => console.log(x), 500);
debouncedFn('pending...');
debouncedFn.cancel(); // nothing will fire
console.log('Cancelled');
Leading-edge debounce fires immediately on the first call, providing instant feedback. The cancel method clears any pending timeout. This is useful for submit buttons where you want immediate response but no double-submissions.
function throttle(fn, interval) {
let lastTime = 0;
let timeoutId = null;
return function (...args) {
const now = Date.now();
const elapsed = now - lastTime;
if (elapsed >= interval) {
// Enough time passed - execute immediately
lastTime = now;
fn.apply(this, args);
} else if (!timeoutId) {
// Schedule trailing call for remaining time
timeoutId = setTimeout(() => {
lastTime = Date.now();
timeoutId = null;
fn.apply(this, args);
}, interval - elapsed);
}
};
}
// Usage: scroll handler
let scrollCount = 0;
const throttledScroll = throttle(() => {
scrollCount++;
console.log(`Scroll handler fired (#${scrollCount})`);
}, 200);
// Simulate rapid scroll events (every 50ms)
const events = [0, 50, 100, 150, 200, 250, 300, 350, 400];
events.forEach((ms, i) => {
setTimeout(() => throttledScroll(), ms);
});
// Fires at: 0ms, 200ms, 400ms - not all 9 times
Throttle executes the function immediately if enough time has passed, otherwise schedules a trailing execution. This guarantees the function fires at regular intervals during sustained activity, unlike debounce which waits for silence.
// Side-by-side comparison with event counts
let debounceCount = 0;
let throttleCount = 0;
let rafCount = 0;
let totalEvents = 0;
const debouncedHandler = debounce(() => {
debounceCount++;
}, 200);
const throttledHandler = throttle(() => {
throttleCount++;
}, 200);
// rAF-based throttle
let rafScheduled = false;
function rafHandler() {
if (!rafScheduled) {
rafScheduled = true;
requestAnimationFrame(() => {
rafCount++;
rafScheduled = false;
});
}
}
// Simulate 100 events over 1 second
for (let i = 0; i < 100; i++) {
setTimeout(() => {
totalEvents++;
debouncedHandler();
throttledHandler();
rafHandler();
}, i * 10);
}
// After ~1100ms, check counts
setTimeout(() => {
console.log(`Total events: ${totalEvents}`);
console.log(`Debounce calls: ${debounceCount}`); // ~1 (only after all stop)
console.log(`Throttle calls: ${throttleCount}`); // ~5 (every 200ms)
console.log(`rAF calls: ${rafCount}`); // ~60 (every frame)
}, 1500);
// Summary:
// Debounce: "Wait until they stop, then do it once"
// Throttle: "Do it at most every N ms during activity"
// rAF: "Do it once per animation frame (~16ms)"
100 events fire over 1 second. Debounce only fires once (after the events stop). Throttle fires ~5 times (every 200ms). requestAnimationFrame fires ~60 times (every frame). Choose based on whether you need post-activity execution, regular updates, or visual synchronization.
// React hook: useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage in a search component
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
// This only fires 300ms after user stops typing
fetch(`/api/search?q=${debouncedQuery}`)
.then(res => res.json())
.then(data => console.log('Results:', data.length));
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
// Throttled scroll with cleanup
function useThrottle(callback, delay) {
const lastRun = useRef(Date.now());
return useCallback((...args) => {
if (Date.now() - lastRun.current >= delay) {
callback(...args);
lastRun.current = Date.now();
}
}, [callback, delay]);
}
In React, useDebounce hook debounces a value that triggers useEffect. The cleanup function in useEffect clears the timer on re-render, providing automatic cancellation. This is the standard pattern for search-as-you-type in React applications.
Debounce, Throttle, setTimeout, clearTimeout, Rate Limiting, requestAnimationFrame