Implement a throttle function that, given a callback fn and a delay wait (ms), returns a new function. The returned function invokes fn immediately on the first call, then ignores every subsequent call for the next wait milliseconds. Once the window passes, the next call invokes fn again and the cycle repeats.
This is the leading-edge variant — calls during the lockout window are dropped, not deferred.
function throttle(fn, wait) {
// returns a new function that, when called repeatedly,
// invokes `fn` at most once per `wait` ms (fire on the leading edge)
}
const log = throttle((msg) => console.log(msg), 100);
log('a'); // fires immediately → 'a'
log('b'); // dropped (within 100ms lockout)
log('c'); // dropped (within 100ms lockout)
// 150ms later…
log('d'); // fires → 'd' (cycle resets)
log('e'); // dropped
A common scroll handler:
window.addEventListener('scroll', throttle(updateHeader, 100));
// updateHeader runs at most 10 times per second, regardless of scroll velocity.
setTimeout / Date.now() — no external libraries.this if possible (forward via apply / call).cancel() method, or a flush() method — those belong to richer variants.You'll build a function that fires immediately on the first call, then refuses to fire again until a fixed window passes — a rate-limiter you can wrap around any callback.
A user is scrolling fast. Your onScroll handler is doing real work — measuring offsets, repositioning a sticky header. If it runs on every scroll event, the page chugs. You want the handler to run at most once every 100ms, no matter how frantic the scrolling. The first call fires right away (so the user sees an immediate response), and any calls in the next 100ms are simply ignored. That's throttle.
Each "fire" opens a lockout window of wait milliseconds. While the window is open, every call is dropped on the floor — no queueing, no deferred execution, just gone. The first call after the window closes fires and opens a fresh window.
The state you need to track is tiny: when did I last fire? Compare every call's timestamp to that, and you have your answer.
The obvious first try uses setTimeout to "schedule" the next fire:
function throttleBroken(fn, wait) {
let scheduled = false;
return function (...args) {
if (scheduled) return;
scheduled = true;
setTimeout(() => {
fn(...args);
scheduled = false;
}, wait);
};
}
This compiles and runs, but it's wrong in two ways. First, it doesn't fire on the leading edge — the first call waits wait ms before firing, which makes the UI feel sluggish. Second, it uses the first call's args when the timer fires, not the most recent — for a scroll handler that needs the current scroll position, that's the wrong value.
The fix isn't to patch this — it's to throw the approach out. We don't need setTimeout at all. We just need a timestamp.
function throttle(fn, wait) {
let lastFiredAt = 0;
return function throttled(...args) {
const now = Date.now();
if (now - lastFiredAt < wait) return; // inside the lockout — drop
lastFiredAt = now;
fn.apply(this, args);
};
}
module.exports = { throttle };
Four lines of meaningful logic. The whole trick is lastFiredAt living in the outer closure. Each call compares now to the last fire-time; if the difference is less than wait, we're still inside the lockout and the call is dropped. Otherwise we stamp lastFiredAt = now and invoke.
lastFiredAt = 0 makes the very first call fire (since Date.now() - 0 is huge).
Say wait = 100 and the user fires four scroll events at t = 0, 30, 80, 150. These t values are relative positions on the timeline (milliseconds since the first call) — not real epoch timestamps off the wall clock. Date.now() itself will return big numbers like 1717280000000; what matters is the difference between them.
lastFiredAt is 0. 0 - 0 = 0 < 100? No — wait, Date.now() at the first call is some huge number like 1717280000000, so now - 0 ≫ 100. The condition is false, we don't drop. Stamp lastFiredAt = 1717280000000. Fire fn. (Translating to our diagram: t=0 corresponds to "fire opens the lockout".)now - lastFiredAt = 30 < 100. Inside lockout. Drop. fn not called.now - lastFiredAt = 80 < 100. Still inside lockout. Drop.now - lastFiredAt = 150, which is not less than 100. Out of lockout. Stamp lastFiredAt = now. Fire fn.So fn ran twice: at t=0 with the first call's args, and at t=150 with the fourth call's args. The calls at t=30 and t=80 vanished.
Complexity: each call is O(1) time (one timestamp compare, one assignment) and O(1) space (a single number held in the closure) — independent of how often throttled is invoked or how large wait is.
lastFiredAt = 0 — must allow the first call to fire. Setting it to Date.now() at construction time would block the first call for wait ms.Date.now(), not performance.now() — both work, but Date.now() survives across iframes and worker contexts more reliably. For animations specifically, requestAnimationFrame is a better tool than throttle anyway.setTimeout needed — the broken first attempt's biggest sin was scheduling deferred work. The leading-edge throttle has no deferred work; it either fires now or never.Real throttle implementations (lodash, underscore) layer on:
.cancel() — discard any pending trailing call.now() function so tests can use a fake clock instead of real Date.now().Each is ~5–10 lines on top of what you have. The trailing-edge variant is the most useful real-world extension — it brings throttle closer in feel to debounce while still firing on the leading edge.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Implement a throttle function that, given a callback fn and a delay wait (ms), returns a new function. The returned function invokes fn immediately on the first call, then ignores every subsequent call for the next wait milliseconds. Once the window passes, the next call invokes fn again and the cycle repeats.
This is the leading-edge variant — calls during the lockout window are dropped, not deferred.
function throttle(fn, wait) {
// returns a new function that, when called repeatedly,
// invokes `fn` at most once per `wait` ms (fire on the leading edge)
}
const log = throttle((msg) => console.log(msg), 100);
log('a'); // fires immediately → 'a'
log('b'); // dropped (within 100ms lockout)
log('c'); // dropped (within 100ms lockout)
// 150ms later…
log('d'); // fires → 'd' (cycle resets)
log('e'); // dropped
A common scroll handler:
window.addEventListener('scroll', throttle(updateHeader, 100));
// updateHeader runs at most 10 times per second, regardless of scroll velocity.
setTimeout / Date.now() — no external libraries.this if possible (forward via apply / call).cancel() method, or a flush() method — those belong to richer variants.You'll build a function that fires immediately on the first call, then refuses to fire again until a fixed window passes — a rate-limiter you can wrap around any callback.
A user is scrolling fast. Your onScroll handler is doing real work — measuring offsets, repositioning a sticky header. If it runs on every scroll event, the page chugs. You want the handler to run at most once every 100ms, no matter how frantic the scrolling. The first call fires right away (so the user sees an immediate response), and any calls in the next 100ms are simply ignored. That's throttle.
Each "fire" opens a lockout window of wait milliseconds. While the window is open, every call is dropped on the floor — no queueing, no deferred execution, just gone. The first call after the window closes fires and opens a fresh window.
The state you need to track is tiny: when did I last fire? Compare every call's timestamp to that, and you have your answer.
The obvious first try uses setTimeout to "schedule" the next fire:
function throttleBroken(fn, wait) {
let scheduled = false;
return function (...args) {
if (scheduled) return;
scheduled = true;
setTimeout(() => {
fn(...args);
scheduled = false;
}, wait);
};
}
This compiles and runs, but it's wrong in two ways. First, it doesn't fire on the leading edge — the first call waits wait ms before firing, which makes the UI feel sluggish. Second, it uses the first call's args when the timer fires, not the most recent — for a scroll handler that needs the current scroll position, that's the wrong value.
The fix isn't to patch this — it's to throw the approach out. We don't need setTimeout at all. We just need a timestamp.
function throttle(fn, wait) {
let lastFiredAt = 0;
return function throttled(...args) {
const now = Date.now();
if (now - lastFiredAt < wait) return; // inside the lockout — drop
lastFiredAt = now;
fn.apply(this, args);
};
}
module.exports = { throttle };
Four lines of meaningful logic. The whole trick is lastFiredAt living in the outer closure. Each call compares now to the last fire-time; if the difference is less than wait, we're still inside the lockout and the call is dropped. Otherwise we stamp lastFiredAt = now and invoke.
lastFiredAt = 0 makes the very first call fire (since Date.now() - 0 is huge).
Say wait = 100 and the user fires four scroll events at t = 0, 30, 80, 150. These t values are relative positions on the timeline (milliseconds since the first call) — not real epoch timestamps off the wall clock. Date.now() itself will return big numbers like 1717280000000; what matters is the difference between them.
lastFiredAt is 0. 0 - 0 = 0 < 100? No — wait, Date.now() at the first call is some huge number like 1717280000000, so now - 0 ≫ 100. The condition is false, we don't drop. Stamp lastFiredAt = 1717280000000. Fire fn. (Translating to our diagram: t=0 corresponds to "fire opens the lockout".)now - lastFiredAt = 30 < 100. Inside lockout. Drop. fn not called.now - lastFiredAt = 80 < 100. Still inside lockout. Drop.now - lastFiredAt = 150, which is not less than 100. Out of lockout. Stamp lastFiredAt = now. Fire fn.So fn ran twice: at t=0 with the first call's args, and at t=150 with the fourth call's args. The calls at t=30 and t=80 vanished.
Complexity: each call is O(1) time (one timestamp compare, one assignment) and O(1) space (a single number held in the closure) — independent of how often throttled is invoked or how large wait is.
lastFiredAt = 0 — must allow the first call to fire. Setting it to Date.now() at construction time would block the first call for wait ms.Date.now(), not performance.now() — both work, but Date.now() survives across iframes and worker contexts more reliably. For animations specifically, requestAnimationFrame is a better tool than throttle anyway.setTimeout needed — the broken first attempt's biggest sin was scheduling deferred work. The leading-edge throttle has no deferred work; it either fires now or never.Real throttle implementations (lodash, underscore) layer on:
.cancel() — discard any pending trailing call.now() function so tests can use a fake clock instead of real Date.now().Each is ~5–10 lines on top of what you have. The trailing-edge variant is the most useful real-world extension — it brings throttle closer in feel to debounce while still firing on the leading edge.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.