Debounce vs throttle: what is the difference?
Debounce runs the function once the activity has stopped for N ms — right for search input. Throttle runs it at most once every N ms during activity — right for scroll and resize.
#Debounce
function debounce(fn, wait = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
};
}
input.addEventListener('input', debounce((e) => search(e.target.value), 300));Typing "python" fires six input events but only one search — 300ms after the last keystroke. Without it you send six requests and may render them out of order.
#Throttle
function throttle(fn, limit = 100) {
let waiting = false;
return (...args) => {
if (waiting) return;
fn(...args);
waiting = true;
setTimeout(() => { waiting = false; }, limit);
};
}
window.addEventListener('scroll', throttle(updateProgress, 100), { passive: true });Scrolling fires dozens of events per second. Throttling caps it at ten, which is plenty for a progress bar and far cheaper.
#Which to use
| Situation | Choose |
|---|---|
| Search-as-you-type | Debounce |
| Autosave a draft | Debounce |
| Validating on input | Debounce |
| Scroll position | Throttle |
| Window resize | Throttle (or debounce the final layout) |
| Infinite scroll trigger | Throttle |
#For scroll, prefer the platform
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { update(); ticking = false; });
}, { passive: true });requestAnimationFrame runs exactly once per frame, in sync with painting — better than any fixed interval. And { passive: true } tells the browser you will not call preventDefault, so scrolling is not blocked waiting for your handler.
Better still, for "is this element visible" questions, use IntersectionObserver and skip scroll handlers entirely.