Throttle Polyfill
Implement throttle from scratch: guarantee a function runs at most once per interval regardless of how often it is called.
JavaScript · Closures · Timers — Pratik Rai ·
Master the debounce technique in JavaScript to control function execution frequency. Essential for optimizing search inputs, resize handlers, and preventing excessive API calls.
Knowing how to write debounce and knowing when to reach for it are different skills. This is about the second one — choosing between debounce, throttle and the platform APIs that often beat both, and avoiding the handful of mistakes that make a correctly-implemented debounce do nothing at all.
For the implementation itself — timers, leading and trailing edges, cancel and flush — see the debounce polyfill challenge.
Most "this fires too often" problems have a better answer than a timer. Work down this list:
Is it a scroll or resize handler that moves something on screen? Use requestAnimationFrame, not a timer. It syncs with the paint cycle, so you never compute a layout that gets thrown away, and it pauses in background tabs.
Are you detecting whether an element is visible? Use IntersectionObserver. Lazy-loading, infinite scroll and scroll-spy navigation all have native implementations that need no scroll listener whatsoever.
Are you reacting to an element changing size? ResizeObserver. It fires only when that element resizes, rather than on every window resize.
Are you waiting for the user to stop typing? Now you want debounce.
Do you need updates while something continuous is happening? You want throttle.
The distinction between the last two: debounce waits for silence, throttle enforces a rate. A search box wants debounce — only the final query matters. A drag handler wants throttle — you need positions during the drag, not after it.
Delay is a UX decision, not a technical one:
Above roughly 400ms, users notice the pause and start wondering whether the app registered their input. If you need a long delay, show a pending indicator.
Recreating the debounced function every render. By far the most common. In React, calling debounce(...) in the component body returns a new function each render, each with its own timer — so nothing is ever debounced. It still works, which is why it survives review.
JSfile.javascript1// Broken 2const handleSearch = debounce((q) => search(q), 300); 3 4// Correct 5const handleSearch = useMemo(() => debounce((q) => search(q), 300), []); 6useEffect(() => handleSearch.cancel, [handleSearch]);
Not cancelling on unmount. A pending call that fires after teardown either wastes a request or touches a component that no longer exists. Any debounced function inside a component needs its cancel wired to the cleanup.
Assuming debounce fixes race conditions. It does not. Debouncing reduces how many requests you send; it says nothing about the order they come back in. With a slow connection, the response for "ca" can arrive after the one for "cat" and overwrite it with stale results.
That needs separate handling — either an AbortController cancelling the previous request, or a request counter you check before applying a response:
JSfile.javascript1const requestId = ++latestRequest.current; 2const data = await search(query); 3if (requestId !== latestRequest.current) return; // a newer request superseded this 4setResults(data);
A production search box needs debounce and cancellation. They solve different problems and neither substitutes for the other.
Worth separating, because the terms get mixed up. Debounce and throttle are client-side UX tools — they shape how often your own code runs. They are not a defence against abuse, and they offer the server no protection: anyone can call your API directly.
If the goal is protecting a resource, you need actual rate limiting on the server. Client-side timers are about not wasting work, not about enforcing limits.
Debounce makes tests time-dependent, which makes them slow and flaky if you wait for real timers. Use fake timers instead:
JSfile.javascript1jest.useFakeTimers(); 2const fn = jest.fn(); 3const debounced = debounce(fn, 300); 4 5debounced(); debounced(); debounced(); 6expect(fn).not.toHaveBeenCalled(); 7 8jest.advanceTimersByTime(300); 9expect(fn).toHaveBeenCalledTimes(1);
Three calls, one execution — that assertion is the definition of debounce, and it catches the "new function every render" bug that a manual browser check will not.
rAF, IntersectionObserver, ResizeObserver often beat a timer.Goal: Implement debounce and throttle functions within 30 minutes. Focus on understanding closures and timing mechanisms.
Continue learning with these related challenges
Implement throttle from scratch: guarantee a function runs at most once per interval regardless of how often it is called.
JavaScript · Closures · Timers — Pratik Rai ·
Implement two async functions A and B, then return their sum using both sequential and parallel Promise execution, observing the time difference.
JavaScript · Promises · Async/Await — Pratik Rai ·
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement throttle from scratch: guarantee a function runs at most once per interval regardless of how often it is called.
JavaScript · Closures · Timers
Pratik Rai ·
Implement two async functions A and B, then return their sum using both sequential and parallel Promise execution, observing the time difference.
JavaScript · Promises · Async/Await
Pratik Rai ·
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills
Pratik Rai ·