Timing Control
MedDebounce and throttle are essential patterns for controlling function execution frequency. Debounce delays execution until activity stops; throttle limits execution rate. Both use closures to maintain timer state.
Interactive Visualization
CodeSetup
1function debounce(fn, wait) {2 let timeoutId;3 return function(...args) {4 clearTimeout(timeoutId);5 timeoutId = setTimeout(6 () => fn.apply(this, args),7 wait8 );9 };10}1112// Usage: search input13const search = debounce(query => {14 fetch(`/api?q=${query}`);15}, 300);
Timeline
0mstime →
CallExecuteCancelled
Closure State
timeoutId:null
Output
—
debounce() creates closure with timeoutId variable
1 / 6
Key Insight: Debounce waits for a "pause" in activity. Each new call resets the timer. Only the LAST call executes.
Key Points
- Debounce: waits for pause in calls before executing (e.g., search input)
- Throttle: executes at most once per interval (e.g., scroll handler)
- Both use closures to persist timer state between calls
- Leading edge: execute immediately on first call
- Trailing edge: execute after the wait period
- Cancel methods allow cleanup on unmount
Code Examples
Basic Debounce
function debounce(fn, wait) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), wait); }; } // Usage: search input const searchInput = document.getElementById('search'); const handleSearch = debounce((query) => { fetch(`/api/search?q=${query}`); }, 300); searchInput.addEventListener('input', (e) => { handleSearch(e.target.value); }); // Only searches 300ms after user stops typing
Each call resets the timer; function runs after activity stops
Debounce with Leading/Trailing
function debounce(fn, wait, options = {}) { let timeoutId; let lastArgs; const { leading = false, trailing = true } = options; return function(...args) { const shouldCallNow = leading && !timeoutId; lastArgs = args; clearTimeout(timeoutId); timeoutId = setTimeout(() => { timeoutId = null; if (trailing && lastArgs) { fn.apply(this, lastArgs); } }, wait); if (shouldCallNow) { fn.apply(this, args); } }; } // Leading: execute immediately, then debounce const saveNow = debounce(save, 1000, { leading: true, trailing: false }); // Trailing (default): wait for pause, then execute const saveAfter = debounce(save, 1000, { leading: false, trailing: true });
Leading fires immediately; trailing fires after wait period
Basic Throttle
function throttle(fn, wait) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= wait) { lastTime = now; fn.apply(this, args); } }; } // Usage: scroll handler const handleScroll = throttle(() => { console.log('Scroll position:', window.scrollY); }, 100); window.addEventListener('scroll', handleScroll); // Logs at most every 100ms while scrolling
Throttle ensures function runs at most once per interval
Throttle with Leading/Trailing
function throttle(fn, wait, options = {}) { let lastTime = 0; let timeoutId; const { leading = true, trailing = true } = options; return function(...args) { const now = Date.now(); const remaining = wait - (now - lastTime); if (remaining <= 0 || remaining > wait) { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } lastTime = now; if (leading || lastTime !== 0) { fn.apply(this, args); } } else if (!timeoutId && trailing) { timeoutId = setTimeout(() => { lastTime = leading ? Date.now() : 0; timeoutId = null; fn.apply(this, args); }, remaining); } }; } // Trailing ensures final call executes after scrolling stops const handleResize = throttle(recalculate, 200, { trailing: true });
Trailing ensures the final invocation is not lost
Debounce with Cancel
function debounce(fn, wait) { let timeoutId; function debounced(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), wait); } debounced.cancel = function() { clearTimeout(timeoutId); timeoutId = null; }; return debounced; } // React usage with cleanup function SearchComponent() { const debouncedSearch = useMemo( () => debounce(search, 300), [] ); useEffect(() => { return () => debouncedSearch.cancel(); // Cleanup on unmount }, [debouncedSearch]); return <input onChange={e => debouncedSearch(e.target.value)} />; }
Cancel method prevents stale executions after unmount
Comparison: Debounce vs Throttle
// User types: a...b...c...d (each 50ms apart) // wait = 200ms // DEBOUNCE: waits for pause, then executes ONCE // Timeline: a--b--c--d-------[execute with 'd'] // Calls: 0, 0, 0, 0, 0, 0, 0, 1 // THROTTLE: executes every 200ms // Timeline: a--b--c--d--[exec]--[exec] // Calls: 1, 0, 0, 0, 1, 0, 0, 1 // Use DEBOUNCE for: // - Search input (wait for user to stop typing) // - Window resize (recalculate after resizing stops) // - Auto-save (save after user stops editing) // Use THROTTLE for: // - Scroll events (update UI at fixed rate) // - Mouse move (track position without overwhelming) // - API rate limiting (max N calls per second)
Debounce: wait for pause. Throttle: limit rate.
Common Mistakes
- Confusing debounce (wait for pause) with throttle (rate limit)
- Forgetting to cancel debounced functions on component unmount
- Not preserving "this" context in the returned function
- Using debounce when throttle is more appropriate (and vice versa)
Interview Tips
- Implement debounce from scratch with cancel method
- Implement throttle from scratch
- Explain when to use debounce vs throttle with examples
- Know how leading and trailing edge options work
Practice Problems
Apply this concept by solving these 2 problems