Skip to content

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).
Events: X X X X X X X
Time: |-------|-------|----
Throttled: ✓ ✓ ✓
Debounced: ✓

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 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
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)
}
}
// Usage
const 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 typing
searchInput.addEventListener('input', debounce(handleSearch, 500))
Events: X X X X X X X X X X X
Time: |-------------------|-----------------|
Debounced: ✓ ✓

The function only executes after the events stop firing for the specified delay.

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
function throttle(func, limit) {
let inThrottle
return function (...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => {
inThrottle = false
}, limit)
}
}
}
// Usage
const handleScroll = () => {
console.log('Scroll position:', window.scrollY)
// Update UI here
}
// Only call handleScroll once every 200ms while scrolling
window.addEventListener('scroll', throttle(handleScroll, 200))
Events: X X X X X X X X X X X X X X X
Time: |-------|-------|-------|-------|
Throttled: ✓ ✓ ✓ ✓ ✓

The function executes at regular intervals while events are firing.

FeatureDebouncingThrottling
ExecutionAfter inactivity periodAt regular intervals
Best forActions after user stopsContinuous actions
CallsOnce after delayMultiple times at intervals
ExampleSearch autocompleteScroll 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)
}
}
}
// Without optimization
let counter = 0
window.addEventListener('scroll', () => {
counter++
console.log('Scroll event fired:', counter)
})
// Result: Hundreds of calls per second 😱
// With throttling
let throttledCounter = 0
window.addEventListener('scroll', throttle(() => {
throttledCounter++
console.log('Throttled scroll:', throttledCounter)
}, 200))
// Result: ~5 calls per second ✅
// With debouncing
let debouncedCounter = 0
window.addEventListener('scroll', debounce(() => {
debouncedCounter++
console.log('Debounced scroll:', debouncedCounter)
}, 200))
// Result: 1 call after scrolling stops for 200ms ✅
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)
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))
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)
})

CSS-Tricks explanation by David Corbacho

Video tutorial by Web Dev Simplified

Lodash documentation: debounce and throttle

Debounce and throttle utilities by Sindre Sorhus: