#Utilities#Polyfill

Throttle Polyfill

Implement throttle from scratch: guarantee a function runs at most once per interval regardless of how often it is called.

By Pratik RaiEasy

Throttling caps how often a function may run. Given a stream of events arriving faster than you can usefully handle them, throttle lets a steady trickle through and discards the rest. It is the right tool for scroll, resize, mousemove and drag — anywhere you need updates during continuous activity rather than after it stops.

Throttle versus debounce

The distinction matters more than any implementation detail:

calls:      ●●●●●●●●●●●●●●●●●●●●
debounce:                       ▼      (once, after the burst)
throttle:   ▼    ▼    ▼    ▼    ▼      (steady, during the burst)

Debounce answers "tell me when they stop." Throttle answers "tell me regularly while they continue."

Debouncing a scroll handler produces no updates at all until scrolling ends — so a scroll-linked progress bar would sit frozen and then jump. Throttling a search input sends a request every N milliseconds whether the query is finished or not. Choosing wrong produces behaviour that is subtly annoying rather than obviously broken, which is why it survives code review.

Two implementations

Timestamp-based — fires immediately, then suppresses until the interval passes:

JSfile.javascript
1function throttle(fn, interval = 200) { 2 let lastCall = 0; 3 4 return function throttled(...args) { 5 const now = Date.now(); 6 if (now - lastCall >= interval) { 7 lastCall = now; 8 return fn.apply(this, args); 9 } 10 }; 11}

Simple and leading-edge, but it drops the final call. If the user stops scrolling mid-interval, the last position is never reported — and the last position is usually the one you care about.

Timer-based — fires on the trailing edge:

JSfile.javascript
1function throttle(fn, interval = 200) { 2 let timeoutId = null; 3 let lastArgs = null; 4 5 return function throttled(...args) { 6 lastArgs = args; 7 if (timeoutId !== null) return; 8 9 timeoutId = setTimeout(() => { 10 timeoutId = null; 11 fn.apply(this, lastArgs); 12 }, interval); 13 }; 14}

This captures the trailing call but delays the first one, so the UI feels laggy on the initial event.

The version you actually want

Most real uses want both edges: respond immediately, and guarantee the final state is reported.

JSfile.javascript
1function throttle(fn, interval = 200, { leading = true, trailing = true } = {}) { 2 let lastCall = 0; 3 let timeoutId = null; 4 let lastArgs = null; 5 let lastThis = null; 6 7 function invoke(time) { 8 lastCall = time; 9 timeoutId = null; 10 fn.apply(lastThis, lastArgs); 11 lastArgs = lastThis = null; 12 } 13 14 function throttled(...args) { 15 const now = Date.now(); 16 if (lastCall === 0 && !leading) lastCall = now; 17 18 const remaining = interval - (now - lastCall); 19 lastArgs = args; 20 lastThis = this; 21 22 if (remaining <= 0) { 23 if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } 24 invoke(now); 25 } else if (trailing && timeoutId === null) { 26 timeoutId = setTimeout(() => invoke(Date.now()), remaining); 27 } 28 } 29 30 throttled.cancel = () => { 31 clearTimeout(timeoutId); 32 timeoutId = null; 33 lastCall = 0; 34 lastArgs = lastThis = null; 35 }; 36 37 return throttled; 38}

Scheduling the trailing call for remaining rather than the full interval is what keeps the cadence even. Naively using interval stretches the gap after every suppressed call and the rate drifts.

When to reach for something else

Throttle is a general-purpose rate limiter, but for anything that paints to the screen, requestAnimationFrame is strictly better:

JSfile.javascript
1function rafThrottle(fn) { 2 let frameId = null; 3 return function (...args) { 4 if (frameId !== null) return; 5 frameId = requestAnimationFrame(() => { 6 frameId = null; 7 fn.apply(this, args); 8 }); 9 }; 10}

This synchronises with the browser's paint cycle instead of guessing at an interval, so you never compute a layout update that gets discarded before it renders. It also pauses automatically in background tabs. For scroll handlers that move elements, this is the correct default — a 16ms throttle is an approximation of what rAF does exactly.

And for the specific case of "tell me when this element enters the viewport", neither is right: IntersectionObserver does it natively with no scroll handler at all.

Edge cases worth knowing

Date.now() versus performance.now(). Date.now() follows the system clock and can jump backwards on an NTP correction, producing a negative remaining and a burst of calls. performance.now() is monotonic. For a UI throttle the difference never matters; for anything measuring elapsed time it does.

Cancel on unmount. Same as debounce — a pending trailing call that fires after teardown is a bug waiting to happen.

Memoise in React. Creating the throttled function inside the render body gives every render its own timer, which means no throttling at all. useMemo with an empty dependency array, and cancel in the effect cleanup.

Passive listeners. Throttling does not stop a scroll handler blocking the compositor. Add { passive: true } to the listener so the browser knows you will not call preventDefault.

Key takeaways

  • Throttle enforces a maximum rate; debounce waits for a pause.
  • Timestamp-only implementations drop the final call — usually the important one.
  • Schedule trailing calls for the remaining time, not the full interval, to avoid drift.
  • Use requestAnimationFrame instead for anything visual.
  • Ship cancel, memoise in React, and mark scroll listeners passive.

Goal: Implement throttle (leading edge). Bonus: add trailing-call capture with the timestamp approach.

Frequently asked questions

Is throttle asked in frontend interviews?
Regularly, and very often immediately after debounce — the pair is a standard way to check whether you understand timers rather than just having memorised one function. Expect to be asked to explain when each is the right choice.
When should I use throttle instead of debounce?
Use throttle when you want a guaranteed rate of execution during continuous activity: scroll position, pointer movement, resize, or anything driving an animation. Use debounce when you only care about the end of the activity, like firing a search request once the user stops typing.
What is the leading and trailing edge in a throttle?
Leading edge means the function runs immediately on the first call, then goes quiet for the interval. Trailing edge means the last call inside the window still runs once the window closes, so a final event is not lost. Many implementations support both, and interviewers often ask you to add whichever one you left out.
What is the most common mistake in a throttle implementation?
Initialising the timestamp to zero. `Date.now() - 0` is enormous, so the first call always passes the interval check — which happens to look correct, and hides the bug until someone tests the boundary. Tracking whether a call is genuinely the first one avoids depending on that accident.

Related Challenges

Continue learning with these related challenges

View All
JavaScript

Debounce

Master the debounce technique in JavaScript to control function execution frequency. Essential for optimizing search inputs, resize handlers, and preventing excessive API calls.

JavaScript · Closures · AsyncPratik Rai ·

JavaScript

Array.prototype.map Polyfill

Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.

JavaScript · Arrays · PolyfillsPratik Rai ·

JavaScript

Array.prototype.filter Polyfill

Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.

JavaScript · Arrays · PolyfillsPratik Rai ·