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.

Beginner
30 minutes – 2 hours
Impact: High
Related
Experiments:rendering

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

DOM Nodes
↓ 95%
Memory
↓ 80%
FPS
↑ 3–5×
Initial Render
↓ 70%

Decision Matrix

Architectural decision guidance for different scenarios.

Scenario: Updating position/size of many elements in a loop based on their current layout.

Recommended:Read-then-write batching with requestAnimationFrame
🔄 Alternatives:CSS Transitions, Web Animations API

⚖️ 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).

Recommended:CSS transitions or Web Animations API
🔄 Alternatives:requestAnimationFrame loop

⚖️ 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 NOT To Use
  • 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.

Review my Vue table for virtualization opportunitiesCopy Prompt