Practical Guide to Debouncing and Throttling in JavaScript
I once built a search feature that made an API call on every keystroke. Seemed fine in development with my fast typing. Then a user typed "javascript frameworks" and our server got hammered with 21 requests in about 2 seconds. We got rate-limited by our own API.
That's when I learned about debouncing and throttling. Same problem, two different solutions. Let me explain when to use which.
The Problem: Too Many Events
Some events fire way more often than you need:
- keyup on a search input - fires every character
- scroll - fires dozens of times per second
- resize - fires constantly while dragging
- mousemove - fires on every pixel movement
If your handler does anything expensive (API call, DOM manipulation, complex calculation), you'll destroy performance.
Debounce: Wait Until They're Done
Think of an elevator. Someone enters, the doors wait 3 seconds to close. Someone else enters at second 2, the timer resets. The doors only close after 3 seconds of no one entering.
Debouncing waits until the user stops doing something, then fires once.
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
// Clear any existing timer
clearTimeout(timeoutId);
// Set a new timer
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage
const searchInput = document.getElementById('search');
const handleSearch = debounce((query) => {
console.log('Searching for:', query);
// API call here
}, 300);
searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
User types "react"... nothing happens. They pause for 300ms... NOW the search fires. One API call instead of five.
Use debounce for: Search inputs, form validation, save-on-change features, resize handlers that recalculate layout.
Throttle: Limit the Rate
Now imagine a bouncer at a club. He lets one person in every 5 seconds, no matter how many are waiting. Constant, predictable flow.
Throttling guarantees a function runs at most once every X milliseconds. Unlike debounce, it fires during the action, not just at the end.
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
// Update progress bar, lazy load images, etc.
}, 100);
window.addEventListener('scroll', handleScroll);
User scrolls like crazy... function fires every 100ms steadily. You get regular updates without being overwhelmed.
Use throttle for: Scroll handlers, drag handlers, game loops, rate-limited APIs, anything where you need regular updates during continuous action.
Quick Comparison
| Scenario | Debounce | Throttle |
|---|---|---|
| Search autocomplete | ✅ | ❌ |
| Scroll progress bar | ❌ | ✅ |
| Window resize layout | ✅ | ❌ |
| Infinite scroll loading | ❌ | ✅ |
| Form auto-save | ✅ | ❌ |
| Button spam prevention | ❌ | ✅ |
requestAnimationFrame for Animations
For anything visual (scroll effects, animations), there's a third option: requestAnimationFrame. It syncs with the browser's repaint cycle (~60fps).
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateParallax(window.scrollY);
ticking = false;
});
ticking = true;
}
});
This is basically throttling to 60fps, but synced with when the browser actually renders. Perfect for visual updates.
Real World: Leading vs Trailing
Sometimes you want the function to fire immediately on the first event, then debounce/throttle subsequent calls. Lodash calls this "leading" vs "trailing" edge.
function debounce(fn, delay, immediate = false) {
let timeoutId;
return function(...args) {
const callNow = immediate && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!immediate) fn.apply(this, args);
}, delay);
if (callNow) fn.apply(this, args);
};
}
// Fire immediately on first click, ignore rapid subsequent clicks
const handleClick = debounce(submitForm, 1000, true);
Should You Use Lodash?
Lodash's _.debounce and _.throttle are battle-tested and handle edge cases I haven't shown (cancel, flush, maxWait). If you're already using Lodash, use them.
But for simple cases, the implementations above work fine. No need to add a dependency for 15 lines of code.
The important thing is understanding the concept. Once you recognize "this event fires too often," you'll know which tool to reach for.