Debouncing and Throttling
- Debounce: run after the events stop for N ms (great for search inputs, resize).
- Throttle: run at most once every N ms (great for scroll, drag, rate-limits).
Visual behavior (combined)
Section titled “Visual behavior (combined)”Events: X X X X X X XTime: |-------|-------|----Throttled: ✓ ✓ ✓Debounced: ✓The concept
Section titled “The concept”Debouncing and throttling are techniques to control how many times we allow a function to be executed over time. They are especially useful when dealing with events that fire rapidly, such as scrolling, resizing, clicks or typing.
The Debounce technique allow us to “group” multiple sequential calls in a single one.
By using [Throttle], we don’t allow to our function to execute more than once every X milliseconds.
The main difference between this and debouncing is that throttle guarantees the execution of the function regularly, at least every X milliseconds.
~ David Corbacho
Debouncing
Section titled “Debouncing”Definition
Section titled “Definition”Debouncing ensures that a function is only executed after a certain period of inactivity. If the function is called again before the delay has passed, the timer resets.
Use cases:
- Search input: Wait until the user stops typing before making an API call
- Window resize: Wait until the user finishes resizing before recalculating layout
- Form validation: Validate input after the user stops typing
Basic example
Section titled “Basic example”function debounce(func, delay) { let timeoutId
return function (...args) { // Clear the previous timeout clearTimeout(timeoutId)
// Set a new timeout timeoutId = setTimeout(() => { func.apply(this, args) }, delay) }}
// Usageconst searchInput = document.querySelector('#search')
const handleSearch = (event) => { console.log('Searching for:', event.target.value) // Make API call here}
// Only call handleSearch 500ms after the user stops typingsearchInput.addEventListener('input', debounce(handleSearch, 500))Debouncing behavior
Section titled “Debouncing behavior”Events: X X X X X X X X X X XTime: |-------------------|-----------------|Debounced: ✓ ✓The function only executes after the events stop firing for the specified delay.
Throttling
Section titled “Throttling”What is throttling?
Section titled “What is throttling?”Throttling ensures that a function is executed at most once in a specified time period, regardless of how many times it’s triggered.
Use cases:
- Scroll events: Update position indicator while scrolling
- Button clicks: Prevent double-submission
- Mouse movement: Track cursor position without overwhelming performance
- API rate limiting: Ensure requests don’t exceed limits
Basic implementation
Section titled “Basic implementation”function throttle(func, limit) { let inThrottle
return function (...args) { if (!inThrottle) { func.apply(this, args) inThrottle = true
setTimeout(() => { inThrottle = false }, limit) } }}
// Usageconst handleScroll = () => { console.log('Scroll position:', window.scrollY) // Update UI here}
// Only call handleScroll once every 200ms while scrollingwindow.addEventListener('scroll', throttle(handleScroll, 200))Throttling behavior
Section titled “Throttling behavior”Events: X X X X X X X X X X X X X X XTime: |-------|-------|-------|-------|Throttled: ✓ ✓ ✓ ✓ ✓The function executes at regular intervals while events are firing.
Key differences
Section titled “Key differences”| Feature | Debouncing | Throttling |
|---|---|---|
| Execution | After inactivity period | At regular intervals |
| Best for | Actions after user stops | Continuous actions |
| Calls | Once after delay | Multiple times at intervals |
| Example | Search autocomplete | Scroll position tracking |
Advanced example with leading and trailing options
Section titled “Advanced example with leading and trailing options”function debounce(func, delay, { leading = false, trailing = true } = {}) { let timeoutId
return function (...args) { const callNow = leading && !timeoutId
clearTimeout(timeoutId)
timeoutId = setTimeout(() => { timeoutId = null if (trailing) { func.apply(this, args) } }, delay)
if (callNow) { func.apply(this, args) } }}
function throttle(func, limit, { leading = true, trailing = true } = {}) { let inThrottle let lastFunc let lastRan
return function (...args) { if (!inThrottle) { if (leading) { func.apply(this, args) } lastRan = Date.now() inThrottle = true
setTimeout(() => { inThrottle = false if (trailing && lastFunc) { lastFunc() } }, limit) } else { lastFunc = () => func.apply(this, args) } }}Performance comparison
Section titled “Performance comparison”// Without optimizationlet counter = 0window.addEventListener('scroll', () => { counter++ console.log('Scroll event fired:', counter)})// Result: Hundreds of calls per second 😱
// With throttlinglet throttledCounter = 0window.addEventListener('scroll', throttle(() => { throttledCounter++ console.log('Throttled scroll:', throttledCounter)}, 200))// Result: ~5 calls per second ✅
// With debouncinglet debouncedCounter = 0window.addEventListener('scroll', debounce(() => { debouncedCounter++ console.log('Debounced scroll:', debouncedCounter)}, 200))// Result: 1 call after scrolling stops for 200ms ✅Practical examples
Section titled “Practical examples”Search with debouncing
Section titled “Search with debouncing”const searchAPI = async (query) => { const response = await fetch(`/api/search?q=${query}`) return response.json()}
const debouncedSearch = debounce(async (event) => { const query = event.target.value if (query.length >= 2) { const results = await searchAPI(query) displayResults(results) }}, 300)
document.querySelector('#search').addEventListener('input', debouncedSearch)Infinite scroll with throttling
Section titled “Infinite scroll with throttling”const loadMoreContent = () => { const { scrollTop, scrollHeight, clientHeight } = document.documentElement
if (scrollTop + clientHeight >= scrollHeight - 100) { console.log('Loading more content...') // Fetch and append more content }}
window.addEventListener('scroll', throttle(loadMoreContent, 250))Save button with debouncing
Section titled “Save button with debouncing”const autoSave = debounce((content) => { console.log('Auto-saving...') fetch('/api/save', { method: 'POST', body: JSON.stringify({ content }), headers: { 'Content-Type': 'application/json' } })}, 1000)
document.querySelector('#editor').addEventListener('input', (event) => { autoSave(event.target.value)})Resources
Section titled “Resources”CSS-Tricks explanation by David Corbacho
Video tutorial by Web Dev Simplified
Lodash documentation: debounce and throttle
Debounce and throttle utilities by Sindre Sorhus: