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
- When event fires, start a timer
- If event fires again before timer ends, reset timer
- 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
- Execute function immediately on first call
- Ignore subsequent calls until time period passes
- 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
Aspect | Debounce | Throttle |
---|---|---|
Execution | After events stop | During events at intervals |
Frequency | Once after delay | Multiple times at fixed rate |
First call | Delayed | Immediate (usually) |
Use case | Wait for completion | Regular updates |
Example | Search as you type | Scroll 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
Related Concepts
- 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