Fixing DOM Layout Thrashing
Eliminate forced synchronous layouts by batching DOM reads and writes using requestAnimationFrame, preventing the browser from recalculating layout on every loop iteration.
The Problem
Layout thrashing occurs when JavaScript alternately reads and writes to the DOM in a tight loop. Each read after a write forces the browser to immediately recalculate layout (a "reflow"), blocking the main thread. A loop of 100 elements can trigger 100 forced synchronous layouts instead of 1.
Common Symptoms
- Janky animations or scroll behavior even on simple pages.
- Chrome DevTools Performance panel shows repeated purple "Layout" bars within a single frame.
- Lighthouse flags "Reduce JavaScript execution time".
- FPS drops to single digits when updating multiple elements simultaneously.
- "Forced reflow while executing JavaScript" warning in browser DevTools.
Root Causes
- Reading layout properties (offsetHeight, getBoundingClientRect, scrollTop) immediately after a DOM write invalidates the layout cache.
- The browser must flush pending style changes and recalculate layout synchronously before returning the value.
- This happens per-iteration in a loop, multiplying the cost linearly.
Typical Expected Improvements
Decision Matrix
Architectural decision guidance for different scenarios.
Scenario: Updating position/size of many elements in a loop based on their current layout.
⚖️ Trade-offs: Requires splitting code into two phases (read all, then write all). Minor refactoring needed.
💡 Why: Batching all reads before all writes ensures the browser only recalculates layout once per frame, not once per element.
Scenario: Animating a property that does not trigger layout (opacity, transform).
⚖️ Trade-offs: Not all properties can be animated on the compositor thread. Width, height, top, left still trigger layout.
💡 Why: Compositor-thread animations bypass the main thread entirely, resulting in zero layout cost.
- When a single one-time DOM measurement is required at page load — a single read is never thrashing.
- When the element count is very small (< 10) and performance impact is negligible.
- When the rAF deferral delay (up to 16ms) breaks a required synchronous user interaction.
Approaches
Do This
- Batch all DOM reads first, store results in variables, then perform all DOM writes.
- Wrap write operations in requestAnimationFrame to defer them to the next paint cycle.
- Use CSS transforms (translateX/Y) instead of top/left for position-based animations.
- Use `will-change: transform` for elements that animate frequently.
Anti-patterns
- Reading layout properties (offsetWidth, getBoundingClientRect) inside the same loop iteration that writes to the DOM.
- Using setInterval for animations — it does not synchronize with the paint cycle.
- Animating layout-triggering CSS properties (width, height, margin) when transform can be used instead.
Implementation
import { useVirtualList } from '@vueuse/core' const { list, containerProps, wrapperProps } = useVirtualList( hugeDataArray, { itemHeight: 48 } )
Use AI
Instantly analyze your codebase using this recipe via MCP. Open your AI agent and run this prompt.