Full guide with an interactive side-by-side demo, React hooks, and a complete decision table on
onlyWebPro.
The one-line difference:
- Debounce — "wait until they've stopped, then act once"
- Throttle — "act on a fixed schedule, no matter what"
Pick the wrong one and your UI either feels broken (debounce on scroll progress) or fires too many API calls (throttle on search input).
Debounce — built from scratch
Every call resets the timer. The function only runs once the events have been quiet for the full delay.
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Only calls the API after 300ms of typing silence
const handleSearch = debounce((query) => fetchResults(query), 300);
searchInput.addEventListener('input', (e) => handleSearch(e.target.value));
Throttle — built from scratch
Checks elapsed time and skips calls that arrive too soon.
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
// Updates scroll progress at most every 100ms
const updateProgress = throttle(() => {
const pct = window.scrollY / (document.body.scrollHeight - window.innerHeight);
progressBar.style.width = (pct * 100) + '%';
}, 100);
window.addEventListener('scroll', updateProgress, { passive: true });
Always add passive: true to scroll listeners that don't call preventDefault() — it's a free performance win.
The critical difference in output behaviour
| Debounce | Throttle | |
|---|---|---|
| Output while events are active | Zero — silent | Regular — fires on schedule |
| Output after events stop | One final call | Nothing |
| Best for | Search, validation, auto-save | Scroll tracking, progress bars, analytics |
This is why debounce is wrong for scroll-linked UI — it shows nothing while the user is actively scrolling.
When to use requestAnimationFrame instead
For anything directly changing visual properties (transform, opacity, parallax), skip throttle entirely. Use requestAnimationFrame — it syncs with the browser's actual paint cycle for smoother results.
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
requestAnimationFrame(() => {
el.style.transform = `translateY(${window.scrollY * 0.5}px)`;
ticking = false;
});
ticking = true;
}, { passive: true });
The three-way decision
- Debounce → search inputs, form validation, auto-save, resize recalculation
- Throttle → scroll progress, analytics, infinite scroll triggers, API rate-limiting
- requestAnimationFrame → parallax, sticky header opacity, canvas animations
The full guide has a live interactive demo where you can click rapidly and watch raw events, debounced output, and throttled output on separate timelines — the clearest way to see how differently they behave.
👉 Read the full guide + interactive demo
Part 3 of the Frontend Performance Series on onlyWebPro.
Which do you reach for more often — debounce or throttle? And has rAF replaced throttle for you on scroll animations?
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.