Michael Ouroumis logoichael Ouroumis

Debounce and Throttle in JavaScript: Controlling Function Execution

Dark blue circuit-patterned background with glowing nodes radiating toward a clear center, overlaid with bold white text ‘Debounce & Throttle’ and the yellow JavaScript logo beneath.

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

FeatureDebounceThrottle
Invocation RateOnce after events stopAt most once per interval
Use-caseBatch rapid input (e.g., search box)Limit continuous events (e.g., scroll)
Leading OptionOptional (call at start)Optional (call at start of interval)
Trailing OptionOptional (call at end of delay)Optional (call at end of interval)
Timer ResettingTimer resets on each callTimer 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 callsdebounce(fetchSuggestions, 300)fetch on every input event
Throttle for scroll handlersthrottle(handleScroll, 100)Direct window.addEventListener('scroll', ...)
Customizable optionsexpose { leading, trailing } flagshard-coded behavior without flexibility
Cancel on unmountdebouncedFn.cancel() in cleanupleaving timers running after component destroyed
Single instance per handlerreuse same debounced/throttled wrapperrecreating 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.

Enjoyed this post? Share it: