Implement a Throttled Resize Handler in JavaScript
Advertisement
🧩 Scenario
Architecture Walkthrough
Timestamp-Based Throttle
A timestamp-based throttle uses Date.now() to track when the wrapped function last executed. On every call, it computes the elapsed time since the last execution. If elapsed time is less than the configured delay, it returns immediately. If elapsed time equals or exceeds the delay, it records the new timestamp and calls the function.
This approach has a critical advantage over setTimeout-based throttle for resize handlers: it does not queue a deferred call. A setTimeout throttle schedules a callback to fire after the delay elapses. If resize events arrive at 60 per second and the delay is 100ms, approximately 6 events are queued. After resize ends, all 6 queued callbacks fire in rapid succession. The timestamp approach has no queue: calls that arrive during the delay window are simply dropped.
requestAnimationFrame Throttle
requestAnimationFrame (rAF) provides a simpler throttle that aligns exactly with the browser's paint cycle (typically 60fps on a 60Hz display). You set a flag when a rAF is pending; if the flag is set, return immediately. When the rAF fires, clear the flag and call the handler. Subsequent resize events during the same frame are dropped.
Because rAF callbacks run just before the browser paints, any layout writes performed inside the callback are batched with the current paint pass. This is optimal for handlers that update CSS or DOM dimensions in response to resize.
Passive Listeners and Cleanup
The resize event on window does not support preventDefault(). Adding { passive: true } tells the browser this and allows it to skip waiting for the handler to complete before beginning scroll or paint processing. Always add passive: true for resize and scroll listeners.
Cleanup is important when the handler is used in a component or module with a teardown phase. Store the exact same function reference that was passed to addEventListener and pass it to removeEventListener. Arrow functions defined inline cannot be removed because each definition creates a new reference.
Key Code Explained
// Option 1: Timestamp-based throttle — no queued callbacks
function createThrottledResizeHandler(handler, delay = 100) {
let lastRan = 0;
const throttled = () => {
const now = Date.now();
if (now - lastRan < delay) return; // still within the throttle window
lastRan = now;
handler(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', throttled, { passive: true });
return {
destroy: () => window.removeEventListener('resize', throttled),
};
}
const resizeHandler = createThrottledResizeHandler((width, height) => {
recalculateLayout(width, height);
redrawChart(width, height);
}, 100);
// On teardown:
resizeHandler.destroy();
// Option 2: requestAnimationFrame — frame-aligned, no setTimeout required
function createRafResizeHandler(handler) {
let rafPending = false;
const onResize = () => {
if (rafPending) return; // rAF already queued for this frame
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
handler(window.innerWidth, window.innerHeight);
});
};
window.addEventListener('resize', onResize, { passive: true });
return {
destroy: () => window.removeEventListener('resize', onResize),
};
}
// Option 3: ResizeObserver — preferred for observing a specific element
function observeElementResize(element, handler) {
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
const { width, height } = entry.contentRect;
handler(width, height);
});
observer.observe(element);
return {
destroy: () => observer.disconnect(),
};
}
// React integration
function useWindowSize(delay = 100) {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const { destroy } = createThrottledResizeHandler((width, height) => {
setSize({ width, height });
}, delay);
return destroy;
}, [delay]);
return size;
}
The ResizeObserver in option 3 is the modern replacement for listening to window resize events when the goal is responding to a specific element's size. Unlike a window resize listener, it fires even when the element's size changes due to CSS changes, parent layout shifts, or content changes, not only on viewport resize.
Tradeoffs
| Approach | Fires rate | Queue buildup | Layout alignment | Best for |
|---|---|---|---|---|
| Unthrottled listener | Every resize pixel | None | None | Nothing (causes jank) |
| Timestamp throttle | At most once per delay | None | No | General recalculation tasks |
| rAF throttle | At most once per frame | None | Yes (pre-paint) | Canvas renders, DOM layout updates |
| ResizeObserver | When element changes | Browser-managed | Yes | Observing a specific element |
What Interviewers Actually Check
- Whether you can implement timestamp-based throttle without relying on
setTimeout - Whether you know requestAnimationFrame as an alternative aligned with the paint cycle
- Whether you can explain the queue problem with
setTimeout-based throttle on a continuous event - Whether you know to add
passive: trueto scroll and resize listeners - Whether you know
ResizeObserveras the modern API for element-level resize detection
Follow-Up Questions
- How does
ResizeObserverdiffer from aMutationObserver, and when would you use each? - If your resize handler performs a canvas
drawImagecall, why is rAF throttle more correct than timestamp throttle? - How would you implement a
useElementSizehook that usesResizeObserverinside React? - What is the
contentBoxSizevsborderBoxSizevsdevicePixelContentBoxSizeon aResizeObserverentry? - How would you handle a case where the resize handler itself takes longer than a single 16ms animation frame?
Common Candidate Mistakes
- Using a
setTimeout-based throttle and not knowing it queues callbacks that fire in a burst after resize ends - Not adding
{ passive: true }to the resize listener, leaving the browser waiting unnecessarily - Defining the handler inline as an arrow function and then calling
removeEventListenerwith a different arrow function reference, which does nothing - Reading
getBoundingClientRectand writing CSS dimensions in alternating order inside the handler, causing forced synchronous layouts - Using debounce when the use case (live chart resize) requires continuous feedback during the resize gesture
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you implement timestamp-based throttle from memory without using
setTimeout? - Can you explain why requestAnimationFrame is well-suited for resize handlers that update the layout?
- Can you describe what the
passiveoption does on an event listener and when to use it? - Can you explain when to use throttle vs debounce for a resize event?
- Can you write a cleanup function that correctly removes the resize listener?
Summary
An unthrottled window resize listener fires dozens of times per second during a resize gesture. If the handler performs layout reads or DOM writes on each call, it causes forced synchronous layouts and visible jank.
Timestamp-based throttle is the most predictable fix: compute elapsed time on each call and skip if within the delay window. Unlike setTimeout-based throttle, it does not queue deferred calls, so no burst of stale callbacks fires after the resize ends. For handlers that update the DOM or canvas, requestAnimationFrame is a better fit: it fires at most once per animation frame, and callbacks run just before painting, allowing layout writes to batch with the current frame.
For component-level resize detection, ResizeObserver is the modern standard. It observes an element directly rather than the window, fires only when that element's dimensions change, handles browser-level throttling, and works correctly with CSS-triggered size changes that have nothing to do with the viewport size.
What is the difference between throttle and debounce for a resize handler?
Throttle fires at a maximum frequency (e.g., once per 100ms) so layout updates happen continuously during resize. Debounce fires only after the user stops resizing. Use throttle when you need live feedback, debounce when you only care about the final size.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement