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?
