Debounce and Throttle in JavaScript: Controlling Function Execution

TL;DR: Debounce delays a function call until a specified amount of time has passed since it was last invoked—ideal for grouping rapid events (e.g., keystrokes). Throttle ensures a function is called at most once in a set interval—perfect for steady-rate invocation (e.g., scroll or resize). Use debounce to batch or wait for “settled” input, throttle to limit continuous events, and choose implementations that support options like leading/trailing invocation.
Why Rate-Limiting Matters
Rapid-fire events—like window resizing, scrolling, or user typing—can trigger dozens or hundreds of handler calls per second, leading to:
- Performance bottlenecks: excessive layout and paint cycles
- Unresponsive UIs: jank, dropped frames, and poor UX
- Unnecessary network requests: spamming APIs with each keystroke
Rate-limiting with debounce and throttle keeps event handlers efficient, smooth, and kind to CPU and network resources.
1. Debounce
Concept
Debounce “waits” until events stop firing for a given delay, then invokes the function once.
- Use-case: search inputs, auto-save, form validation
- Behavior: resets the timer on each call; only the last invocation executes
Basic Implementation
function debounce(fn, delay = 300) { let timerId return function (...args) { clearTimeout(timerId) timerId = setTimeout(() => { fn.apply(this, args) }, delay) } } // Usage const onSearch = debounce((query) => { fetch(`/api/search?q=${query}`).then(renderResults) }, 500) inputElement.addEventListener('input', (e) => onSearch(e.target.value))
Options
- Immediate/Leading: invoke at the start, then ignore until delay
- Trailing: invoke after the delay (default)
function debounce(fn, delay, { leading = false, trailing = true } = {}) { let timerId return function (...args) { const callNow = leading && !timerId clearTimeout(timerId) timerId = setTimeout(() => { timerId = null if (trailing) fn.apply(this, args) }, delay) if (callNow) fn.apply(this, args) } }
2. Throttle
Concept
Throttle ensures a function is invoked at most once every specified interval.
- Use-case: scroll listeners, window resize, drag events
- Behavior: invokes immediately, then blocks subsequent calls until interval passes
Basic Implementation
function throttle(fn, interval = 200) { let lastTime = 0 return function (...args) { const now = Date.now() if (now - lastTime >= interval) { lastTime = now fn.apply(this, args) } } } // Usage const onScroll = throttle(() => { console.log('Scroll position:', window.scrollY) }, 100) window.addEventListener('scroll', onScroll)
Options
- Leading/Trailing control similar to debounce, enabling calls at start/end of interval.
function throttle(fn, interval, { leading = true, trailing = true } = {}) { let lastTime = 0, timerId return function (...args) { const now = Date.now() if (!lastTime && !leading) lastTime = now const remaining = interval - (now - lastTime) if (remaining <= 0) { clearTimeout(timerId) lastTime = now fn.apply(this, args) } else if (trailing) { clearTimeout(timerId) timerId = setTimeout(() => { lastTime = leading ? Date.now() : 0 fn.apply(this, args) }, remaining) } } }
3. Key Differences
Feature | Debounce | Throttle |
---|---|---|
Invocation Rate | Once after events stop | At most once per interval |
Use-case | Batch rapid input (e.g., search box) | Limit continuous events (e.g., scroll) |
Leading Option | Optional (call at start) | Optional (call at start of interval) |
Trailing Option | Optional (call at end of delay) | Optional (call at end of interval) |
Timer Resetting | Timer resets on each call | Timer does not reset after invocation |
4. When to Use Which
-
Debounce
- Wait for user to finish typing before validating or fetching.
- Manage resize events only after resizing stops.
-
Throttle
- Update UI on scroll, but not on every pixel movement.
- Track window resize progress at a steady rate.
5. Real-World Examples
- Typeahead Search: debounce server requests for suggestions
- Infinite Scroll: throttle load-more checks on scroll
- Autosave Drafts: debounce saves to avoid spamming backend
- Drag-and-Drop: throttle UI position updates for performance
6. Best Practices & Anti-Patterns
Practice | ✅ Good Use | ❌ Anti-Pattern |
---|---|---|
Debounce for endpoint calls | debounce(fetchSuggestions, 300) | fetch on every input event |
Throttle for scroll handlers | throttle(handleScroll, 100) | Direct window.addEventListener('scroll', ...) |
Customizable options | expose { leading, trailing } flags | hard-coded behavior without flexibility |
Cancel on unmount | debouncedFn.cancel() in cleanup | leaving timers running after component destroyed |
Single instance per handler | reuse same debounced/throttled wrapper | recreating wrapper inside listener on each event |
Final Thoughts
- Choose debounce when you need to wait until input quiets down.
- Choose throttle when you need regular, spaced-out updates.
- Expose leading/trailing options for maximum flexibility.
- Always clean up timers in component unmounts or teardown.
With debounce and throttle in your toolbox, you’ll deliver smoother, more efficient JavaScript applications.