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?