Retry Decorator
A reusable decorator that adds exponential backoff retry logic to any function without modifying its…
Limit function execution to fixed intervals during continuous events like scroll, drag, and resize.
function throttle(fn, intervalMs) {
let lastTime = 0;
let timerId = null;
return function (...args) {
const now = Date.now();
const elapsed = now - lastTime;
if (elapsed >= intervalMs) {
// Enough time passed — fire immediately
lastTime = now;
fn.apply(this, args);
} else {
// Schedule a trailing call so the last event is never lost
clearTimeout(timerId);
timerId = setTimeout(() => {
lastTime = Date.now();
fn.apply(this, args);
}, intervalMs - elapsed);
}
};
}
Let us trace what happens when a user scrolls continuously for 1 second with a 200ms throttle:
Time 0ms: scroll event → elapsed >= 200ms? Yes (first call) → FIRE
Time 50ms: scroll event → elapsed = 50ms, < 200ms → schedule trailing
Time 100ms: scroll event → elapsed = 100ms → re-schedule trailing
Time 150ms: scroll event → elapsed = 150ms → re-schedule trailing
Time 200ms: scroll event → elapsed >= 200ms → FIRE immediately
Time 250ms: scroll event → elapsed = 50ms → schedule trailing
...
Time 1000ms: scroll event → elapsed >= 200ms → FIRE
Time 1050ms: scrolling stops
Time 1200ms: trailing timer fires → FIRE (last position captured)
Without throttle: ~60 calls (every 16ms at 60fps)
With 200ms throttle: ~6 calls — evenly spaced, last event captured
The trailing call is what separates a good throttle from a naive one. Without it, the last event before the user stops gets silently dropped. That means scroll position, drag position, or resize dimensions could be stale. The trailing timer guarantees the final value always gets processed.
Debounce: waits until activity STOPS, then fires once
→ Best for: search input, form validation, auto-save
Throttle: fires at regular INTERVALS during activity
→ Best for: scroll, drag, resize, mousemove
Scroll handler without throttle:
60 position checks per second → layout thrashing → dropped frames
Scroll handler with 100ms throttle:
10 position checks per second → smooth infinite scroll loading
// Infinite scroll — check position every 200ms, not every frame
const handleScroll = throttle(() => {
const scrollBottom = window.innerHeight + window.scrollY;
const docHeight = document.documentElement.scrollHeight;
if (docHeight - scrollBottom < 500) {
loadMoreItems();
}
}, 200);
window.addEventListener('scroll', handleScroll);
// Drag handler — update position smoothly without overload
const handleDrag = throttle((e) => {
updateElementPosition(e.clientX, e.clientY);
sendPositionToServer(e.clientX, e.clientY);
}, 50);
element.addEventListener('mousemove', handleDrag);
// Analytics — track engagement without flooding the endpoint
const trackScroll = throttle(() => {
analytics.track('scroll_depth', getScrollPercentage());
}, 2000);
Use throttle when you need regular updates during continuous user activity. The interval depends on the use case: 50 to 100 milliseconds for visual feedback like drag handlers, 200 to 500 milliseconds for data loading like infinite scroll, and 1000 milliseconds or more for analytics tracking. If you only care about the final value after activity stops, use debounce instead.