Performance and DevTools

Debounce and throttle repeated input

Choose between waiting for a burst to finish and limiting work to a steady maximum rate.

Some events arrive in bursts. A person typing fires an input event on every keystroke. A scroll fires dozens of events per second. Doing real work on each one, like a network request, is wasteful and often wrong.

Debounce and throttle are the two ways to handle this. They sound similar, but they express two different product decisions.

Debounce: wait for the burst to end

A debounce waits until the events have stopped for a chosen delay, then runs once:

let timer

search.addEventListener('input', () => {
  clearTimeout(timer)
  timer = setTimeout(() => runSearch(search.value), 250)
})

Every keystroke cancels the pending timer and starts a new one. runSearch() runs only when the user pauses for 250 milliseconds. Type “javascript” quickly and you get one search, not ten.

This fits autocomplete, because nobody wants results for “j”, “ja”, “jav”. The cost is latency on purpose: nothing happens until the pause.

Throttle: run at most every N milliseconds

A throttle lets the work run at a fixed maximum rate while the events keep coming:

let last = 0

window.addEventListener('scroll', () => {
  const now = Date.now()
  if (now - last < 200) return
  last = now
  updateProgressBar(window.scrollY)
})

The user scrolls continuously, and updateProgressBar() runs about five times per second. It fits reading-progress indicators and analytics, anything that must update during the activity without processing every single event.

Notice that a throttle drops the events in between. If the last scroll event lands 50 milliseconds after the previous run, it’s ignored, and the bar can end slightly off. Some implementations run one trailing call to fix that.

Or schedule one frame

For visual work tied to scroll or pointer position, there’s a third option we saw in the previous lesson: save the latest value and apply it once in requestAnimationFrame(). It updates exactly once per frame, which is the most often anyone can see, and it never builds a backlog of stale positions.

Pick from the behavior you want

  • Only the final value after a burst matters? Debounce.
  • You need periodic updates during the burst? Throttle.
  • You need the latest visual state every frame? Schedule one frame callback.

Clean up

Both patterns leave something pending: a timer or a frame request. If the component that created it goes away, that pending call fires against a DOM that no longer exists. Cancel it in your cleanup with clearTimeout() or cancelAnimationFrame().

Decide on the edges too. Should the first event in a burst always run right away? Should the last one always run? Those two answers change the implementation.

To check your choice, record a fast typing or scroll session in the Performance panel before and after. Handler work should drop a lot. If the interface starts to feel disconnected from the input, your delay is too long.

Lesson completed