QuestionsWeb Performance

Avoiding layout thrash

Layout / ReflowMediumWeb Performance

This loop that reads offsetHeight and then sets style each iteration is slow. What is 'layout thrashing' and how do you fix it?

What it tests

Understanding forced synchronous layout and how interleaved reads/writes trigger it repeatedly.

Approach & answer

Layout thrashing is repeatedly forcing the browser to recompute layout inside a loop by interleaving DOM reads and writes. Normally the browser batches style changes and does layout once, lazily, before the next paint. But certain property reads — offsetHeight, offsetTop, getBoundingClientRect(), scrollTop, getComputedStyle, clientWidth, etc. — require an up-to-date layout to answer. If you've written to the DOM since the last layout, reading one of these forces a SYNCHRONOUS layout right now (a 'forced reflow') to flush pending changes. So a loop that goes read → write → read → write invalidates layout on every write and forces a full recompute on every read — O(n) layouts instead of one. The fix is to BATCH: do all your reads first (measure everything), then do all your writes (mutate everything). That way layout is computed at most once for the reads and once (deferred to next frame) for the writes. Concretely: read all offsetHeights into an array, then apply all the new styles. For animations, do the writing inside requestAnimationFrame so it aligns with the frame and reads happen before writes. Libraries formalize this as 'read/write phases' (e.g., FastDOM). Also prefer properties that don't trigger layout at all when animating — transform and opacity are composited and skip layout/paint — over animating top/left/width/height which reflow every frame. The signature symptom in DevTools is a 'Forced reflow' warning or a Performance panel full of purple layout bars inside a script call.

Use this technique when

Fixing a slow loop that measures and mutates the DOM; smoothing scroll/resize handlers.

Complexity

Batching turns O(n) forced layouts into O(1)

Code

// BAD: read forces layout, write invalidates it -> reflow every iteration
boxes.forEach((box) => {
  const h = box.offsetHeight;      // READ -> forced synchronous layout
  box.style.height = h * 2 + 'px'; // WRITE -> invalidates layout
});

// GOOD: batch all reads, then all writes -> layout computed once
const heights = boxes.map((box) => box.offsetHeight); // all READS
boxes.forEach((box, i) => {                            // all WRITES
  box.style.height = heights[i] * 2 + 'px';
});

// Animate composited props (no layout/paint) instead of top/left/width:
el.style.transform = 'translateX(100px)'; // GPU, skips layout
// el.style.left = '100px';               // reflows every frame

References