Skip to Content
UX Patterns for Devs GPT is now available! Read more →
GlossaryThrottle and Debounce

Throttle and Debounce

Throttle and debounce are two similar but distinct techniques used to control how many times a function is executed over time. They’re essential for optimizing performance when dealing with events that can fire many times in quick succession, such as scrolling, resizing, typing, or mouse movement.

The Problem They Solve

Without rate limiting, event handlers can fire hundreds of times per second:

// BAD: Function runs on every scroll event (potentially 100+ times/second) window.addEventListener('scroll', () => { expensiveCalculation(); // Performance killer! }); // BAD: API call on every keystroke searchInput.addEventListener('input', (e) => { searchAPI(e.target.value); // Too many requests! });

Debounce

Debounce ensures a function only executes after a specified period of inactivity. It delays execution until after events have stopped firing.

How Debounce Works

  1. When event fires, start a timer
  2. If event fires again before timer ends, reset timer
  3. Only execute function when timer completes

Implementation

function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; } // Usage const searchInput = document.getElementById('search'); const debouncedSearch = debounce((query) => { console.log('Searching for:', query); performSearch(query); }, 300); searchInput.addEventListener('input', (e) => { debouncedSearch(e.target.value); });

Visual Timeline

Events: x--x--x--x----------------x--x------- Debounce: F F └─ Function executes 300ms after last event

Common Use Cases

  • Search input - Wait for user to stop typing
  • Form validation - Validate after user finishes
  • Window resize - Recalculate layout after resizing ends
  • Auto-save - Save after user pauses editing

Throttle

Throttle ensures a function executes at most once in a specified time period. It limits execution frequency but guarantees regular execution during continuous events.

How Throttle Works

  1. Execute function immediately on first call
  2. Ignore subsequent calls until time period passes
  3. Execute again after cooldown period

Implementation

function throttle(func, limit) { let inThrottle; let lastArgs; let lastThis; return function(...args) { lastArgs = args; lastThis = this; if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; // Execute with latest args/context if called during throttle if (lastArgs) { func.apply(lastThis, lastArgs); lastArgs = null; lastThis = null; } }, limit); } }; } // Usage with trailing call option function throttleWithTrailing(func, limit) { let inThrottle; let lastArgs; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; if (lastArgs) func.apply(this, lastArgs); lastArgs = null; }, limit); } else { // Record only when calls occur during cooldown lastArgs = args; } }; } // Usage const handleScroll = throttle(() => { console.log('Scroll position:', window.scrollY); updateScrollIndicator(); }, 100); window.addEventListener('scroll', handleScroll);

Visual Timeline

Events: x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x Throttle: F-------F-------F-------F------ └─ Function executes at regular intervals

Common Use Cases

  • Scroll events - Update UI during scrolling
  • Mouse move tracking - Track position periodically
  • API rate limiting - Prevent exceeding API limits
  • Game loop updates - Fixed frame rate updates
  • Progress updates - Regular status updates

Key Differences

AspectDebounceThrottle
ExecutionAfter events stopDuring events at intervals
FrequencyOnce after delayMultiple times at fixed rate
First callDelayedImmediate (usually)
Use caseWait for completionRegular updates
ExampleSearch as you typeScroll position

Advanced Implementations

Debounce with Immediate Option

function debounce(func, wait, immediate) { let timeout; return function(...args) { const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(() => { timeout = null; if (!immediate) func.apply(this, args); }, wait); if (callNow) func.apply(this, args); }; } // Execute immediately on first call, then debounce const saveDocument = debounce(save, 1000, true);

Throttle with Leading and Trailing

function throttle(func, wait, options = {}) { let timeout, context, args, result; let previous = 0; const later = function() { previous = options.leading === false ? 0 : Date.now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function(..._args) { const now = Date.now(); if (!previous && options.leading === false) previous = now; const remaining = wait - (now - previous); context = this; args = _args; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; }

Cancel and Flush Methods

function debounceWithControls(func, wait) { let timeout; function debounced(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); } debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; debounced.flush = function(...args) { clearTimeout(timeout); func.apply(this, args); }; return debounced; } // Usage const debouncedSave = debounceWithControls(save, 1000); // Force immediate execution debouncedSave.flush(); // Cancel pending execution debouncedSave.cancel();

Real-World Examples

Search Autocomplete

// Debounce to avoid too many API calls const searchSuggestions = debounce(async (query) => { const results = await fetch(`/api/search?q=${query}`); displaySuggestions(await results.json()); }, 300); searchInput.addEventListener('input', (e) => { searchSuggestions(e.target.value); });

Infinite Scroll

// Throttle to check scroll position periodically const checkScrollPosition = throttle(() => { const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 100; if (nearBottom) { loadMoreContent(); } }, 200); window.addEventListener('scroll', checkScrollPosition);

Resize Handler

// Debounce to recalculate after resizing stops const handleResize = debounce(() => { recalculateLayout(); updateCharts(); }, 250); window.addEventListener('resize', handleResize);

Libraries

Lodash

import { debounce, throttle } from 'lodash'; const debouncedFunc = debounce(func, 300); const throttledFunc = throttle(func, 300);

Underscore

const debouncedFunc = _.debounce(func, 300); const throttledFunc = _.throttle(func, 300);

Performance Impact

Without Rate Limiting

// Can fire 100+ times per second window.addEventListener('scroll', () => { // Each execution: getBoundingClientRect(); // Triggers reflow updateStyles(); // Triggers repaint sendAnalytics(); // Network request });

With Rate Limiting

// Fires max 10 times per second window.addEventListener('scroll', throttle(() => { // Reduced from 100+ to 10 executions/second // Can significantly reduce CPU usage }, 100));

Common Mistakes

Using Wrong Technique

// Wrong: Throttle for search (executes during typing) searchInput.addEventListener('input', throttle(search, 300) // ❌ Searches while typing ); // Right: Debounce for search (waits for pause) searchInput.addEventListener('input', debounce(search, 300) // ✅ Searches after typing stops );

Forgetting to Clean Up

// Memory leak risk useEffect(() => { const handleScroll = throttle(updatePosition, 100); window.addEventListener('scroll', handleScroll); // Must remove the same reference return () => { window.removeEventListener('scroll', handleScroll); }; }, []);

Best Practices

Do’s ✅

  • Choose appropriate delays (typically 100-300ms)
  • Use debounce for input validation
  • Use throttle for continuous events
  • Clean up event listeners
  • Consider using established libraries
  • Test on slow devices

Don’ts ❌

  • Don’t use delays that are too short (< 50ms)
  • Don’t use delays that are too long (> 1000ms for interactions)
  • Don’t forget the user experience
  • Don’t throttle critical safety functions
  • Don’t debounce time-sensitive operations
  • DOM - Events that need throttling/debouncing
  • Lazy Loading - Another performance technique
  • RequestAnimationFrame - Alternative for animations
  • Event Delegation - Reduces number of listeners

Key Takeaways

  • Debounce delays execution until after events stop
  • Throttle limits execution to fixed intervals
  • Both improve performance for high-frequency events
  • Choose based on use case: completion vs. progress
  • Critical for smooth user interfaces
  • Small implementation, big performance impact

Stay updated with new patterns

Get notified when new UX patterns are added to the collection.

Last updated on