If your search box stutters as you type, or your page jerks while scrolling, the cause is usually the same: an event handler running far too often. The browser fires input, scroll, and resize events dozens of times per second, and each call does real work. Debouncing and throttling are the two techniques that bring this under control. This article explains how each one behaves, when to reach for which, and the mistakes that quietly break them.
The problem: events fire faster than you can handle
A single drag of the scrollbar can fire the scroll event many times in under a second. If your handler recalculates layout, queries the DOM, or sends a network request each time, you stack up work the browser cannot finish before the next event arrives. The result is jank: dropped frames, delayed input, and in bad cases a frozen tab. Debounce and throttle both reduce how often the expensive work runs, but they do it in opposite ways.
Debounce: wait until the activity stops
Debouncing delays the work until the events go quiet for a chosen period. Every new event resets the timer. The function only runs once the user pauses.
Think of an elevator that waits a few seconds after the last person steps in before closing the doors. If someone keeps entering, the doors keep waiting. Debounce is ideal when you only care about the final state, not the journey.
- Search-as-you-type: wait until the user stops typing before hitting the API.
- Auto-saving a form draft after edits settle.
- Validating a field once the user is done, not on every keystroke.
Throttle: run at a steady maximum rate
Throttling guarantees the function runs at most once per interval, no matter how many events fire. It does not wait for silence; it enforces a rhythm.
Think of a turnstile that lets one person through per second. Throttle is right when you need regular updates during a continuous action.
- Updating a scroll-position progress bar.
- Repositioning elements during a drag.
- Firing analytics on scroll depth without flooding the server.
Side-by-side comparison
| Aspect | Debounce | Throttle |
| Runs when | After activity stops | At a fixed interval during activity |
| Best for | Final value / settled state | Smooth ongoing feedback |
| Risk | Nothing runs until the user pauses | Still runs fairly often |
| Typical delay | 200-500 ms | 50-150 ms |
A real scenario
On a product page I worked on, an autocomplete field sent a request on every keystroke. Typing “laptop stand” produced eleven requests, and slow responses arrived out of order, so results flickered and sometimes showed matches for “lapt”. Switching to a 300 ms debounce cut it to a single request after the user paused. The flicker vanished, and server load dropped sharply. A throttle would have been wrong here because we did not need intermediate results, only the final query.
Common mistakes and how to fix them
- Creating the debounced function inside the handler. If you call
debounce(fn)on every event, you get a brand-new timer each time and nothing ever fires. Create it once, outside the event, and reuse the returned function. - Choosing the wrong tool. Debouncing a scroll progress bar makes it update only after scrolling stops, which looks broken. Use throttle for continuous feedback.
- Delays that feel wrong. A debounce over ~500 ms on search feels sluggish; under ~150 ms barely helps. Test with real typing speed.
- Forgetting the trailing call. A naive throttle can drop the very last event, leaving the final state stale. Make sure your implementation runs once more after the last interval.
- Not cleaning up. In single-page apps, cancel pending timers when a component unmounts, or a late callback may touch elements that no longer exist.
Action steps
- List each high-frequency handler in your app: input, scroll, resize, mousemove, drag.
- For each, ask: do I need the final state (debounce) or steady updates (throttle)?
- Create the wrapped function once and store the reference.
- Start with 300 ms for debounce and 100 ms for throttle, then tune by feel.
- Add cleanup so pending calls are cancelled on teardown.
Conclusion
Debounce waits for quiet; throttle keeps a steady beat. Pick based on whether you care about the end result or the ongoing motion. Your next step: open your slowest page, find the handler firing most often, and wrap it with the right one. You will usually feel the difference immediately.
FAQ
Can I use both together?
Yes. A common pattern throttles updates during an action for smoothness, then debounces a final “settled” callback to save state or send one clean request.
Do I need a library?
No. Both are a few lines of JavaScript. Libraries like Lodash offer well-tested versions with trailing and leading options, which is handy but not required.
What delay should I start with?
Around 300 ms for search debounce and about 100 ms for scroll throttle are sensible defaults. Adjust based on how responsive it feels with real use.
Does this replace pagination or virtualization?
No. Debounce and throttle control how often handlers run. If you are rendering thousands of DOM nodes, you also need virtualization. They solve different problems.
References
- MDN Web Docs: EventTarget.addEventListener and the events section.
- Google web.dev: guidance on scroll performance and input responsiveness.