ResizeObserver: The 'Loop Limit Exceeded' Error
Benign but scary. Learn why modifying layout in a ResizeObserver callback triggers browser loop protection.
The "Scariest" Benign Error
If you monitor frontend errors in production (Sentry, Datadog), you've seen it:
ResizeObserver loop limit exceeded.
It often dominates the error logs, yet users rarely complain.
It often dominates the error logs, yet users rarely complain.
This error reveals a fundamental truth about the browser's Rendering Loop that most developers miss.
The Infinite Loop Trap
The Cycle
To understand why this happens, you must understand when ResizeObserver fires. It doesn't fire during standard JS execution; it fires after Layout but before Paint.
- Script: React updates state.
- Style/Layout: Browser calculates geometry.
- ResizeObserver: Browser notices a size change and fires your callback.
- Your Code: You update state in the callback (e.g.,
setWidth(entry.contentRect.width)). - Re-Layout: React re-renders, causing a new layout calculation.
Crucially, this re-layout happens within the same frame. If this new layout triggers another size change, ResizeObserver would fire again, creating an infinite loop that freezes the browser.
The browser protects itself by stopping this cycle immediately if it detects a loop within the same frame, throwing the error. It renders the first layout and discards the second one until the next frame.
The Solution: Defer to Next Frame
To fix this (and the potential UI flickering), you must ensure your state update doesn't trigger a synchronous re-layout
in the same frame. The tool for this is requestAnimationFrame.
By wrapping your state update in requestAnimationFrame, you tell the browser:
"Finish painting this frame, then process my update." This moves the re-layout to the start
of the next frame, breaking the loop.
Code Example
// ❌ BAD: Triggers synchronous re-layout in the same frame
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// This causes immediate re-layout -> Loop Limit Error
setWidth(entry.contentRect.width);
}
});
// ✅ GOOD: Defers update to the next frame
const resizeObserver = new ResizeObserver((entries) => {
// Wrap in RAF to break the loop
window.requestAnimationFrame(() => {
if (!Array.isArray(entries) || !entries.length) return;
for (const entry of entries) {
setWidth(entry.contentRect.width);
}
});
});Debounce vs RAF
A debounce is often suggested, but requestAnimationFrame is superior here.
Debounce introduces an arbitrary delay (e.g., 100ms) which causes visible lag.
RAF delays exactly until the next frame (~16ms), ensuring smooth updates without the error.
Interactive Demo
Visualize the safe vs. unsafe update patterns:
Advertisement
Explore these curated resources to deepen your understanding
Related Insights
Explore related edge cases and patterns
Advertisement