Timers, Debounce and Throttle
IntermediatesetTimeout and setInterval schedule work; debounce and throttle limit how often it happens. Both patterns are closures, and both need cleanup.
Overview
Timers are the simplest async primitive and the easiest to leak. setInterval keeps firing until cleared, so a component that starts one and unmounts without clearing leaves a callback running against dead state forever. Debounce and throttle are the two rate-limiting patterns you will implement or import constantly: debounce waits for a pause and is right for search-as-you-type, throttle enforces a maximum rate and is right for scroll and resize.
Timers and Cleanup
The delay is a minimum, not a guarantee — the callback runs when the stack is clear and its turn arrives.
const id = setTimeout(fn, 1000)
clearTimeout(id)
const iid = setInterval(fn, 1000)
clearInterval(iid) // otherwise it runs forever
// The leak, in React:
useEffect(() => {
const id = setInterval(tick, 1000)
return () => clearInterval(id) // without this, every mount adds a timer
}, [])
// The delay is a floor. This does not print after exactly 100ms:
setTimeout(() => console.log('later'), 100)
blockFor(500) // the timer waits for the stack to clear
// Nested timers are clamped to >= 4ms after 5 levels of nesting.Debounce — Wait for the Pause
Runs once, after the caller stops calling. The right choice for search inputs and autosave.
function debounce(fn, ms = 300) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), ms)
}
}
// Typing "react" fires one request, not five
const search = debounce(q => fetchResults(q), 300)
input.addEventListener('input', e => search(e.target.value))
// With cancellation, which matters on unmount:
function debounce(fn, ms = 300) {
let timer
const wrapped = (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), ms)
}
wrapped.cancel = () => clearTimeout(timer)
return wrapped
}Throttle — At Most Once Per Interval
Runs at a steady maximum rate while calls keep coming. The right choice for scroll, resize and mousemove.
function throttle(fn, ms = 200) {
let last = 0
return (...args) => {
const now = Date.now()
if (now - last >= ms) {
last = now
fn(...args)
}
}
}
window.addEventListener('scroll', throttle(updateHeader, 200))
// Choosing between them:
// debounce -> "tell me when they have finished" (search, autosave, resize-end)
// throttle -> "tell me regularly while it happens" (scroll position, drag)
// For animation, neither: use requestAnimationFrame, which syncs to paint
const onScroll = () => requestAnimationFrame(updateParallax)Key Points to Remember
- 1A timer delay is a minimum — the callback runs once the stack is clear, not at an exact time
- 2setInterval runs until cleared; not clearing it on unmount is a real and common leak
- 3Debounce fires once after calls stop — right for search-as-you-type and autosave
- 4Throttle fires at most once per interval while calls continue — right for scroll and resize
- 5For visual updates use requestAnimationFrame instead, since it synchronises with the browser paint
Interview Questions
Sign in to ask AriaWhat is the difference between debounce and throttle, and when would you use each?
Implement throttle from scratch.
Why is setTimeout(fn, 100) not guaranteed to run after exactly 100ms?
Ask Aria about Timers, Debounce and Throttle
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.