awesome-everything RU
↑ Back to the climb

Browser & Frontend Runtime

Layout thrash: forced synchronous layout

Crux How reading geometry after writing styles forces an immediate layout flush, what N forced layouts per frame look like in DevTools, and the two-pass batch fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

A resize handler loops over 5000 rows. Each row reads its width then writes a new width. The page blocks for 5 seconds. The CPU is doing nothing exotic — it is running the same layout calculation 5000 times, once per row, because no one told it to batch.

How forced synchronous layout works

Layout is dirty-flag-driven: the browser batches style writes and only flushes them when it needs the latest geometry. The pathological case is a read-then-write loop in JS:

for (const row of rows) {
  const w = row.offsetWidth;        // read → forces flush of pending writes
  row.style.width = w + 10 + 'px'; // write → marks layout dirty again
}

Each iteration forces the browser to compute layout to answer offsetWidth, then immediately dirties layout for the next iteration. N rows = N full layouts.

A 5000-row list at 1 ms per layout = 5 seconds of main-thread blocking. DevTools shows this as a violet “Layout” bar followed by a “Forced reflow while executing JavaScript took XX ms” console warning.

Dirty-flag lifecycle in a read-then-write loop

Iteration 1: write style.width (layout dirty)
Iteration 2: read offsetWidth → FLUSH layout (1 full layout)
Iteration 2: write style.width (layout dirty again)
Iteration 3: read offsetWidth → FLUSH layout (2nd full layout)
… × N iterations = N layouts

The two-pass batch fix

Separate all reads from all writes:

// Two-pass batch: read first, write second.
const widths = [];
for (const row of rows) {
  widths.push(row.offsetWidth);          // pass 1: all reads
}
for (let i = 0; i < rows.length; i++) {
  rows[i].style.width = (widths[i] + 10) + 'px'; // pass 2: all writes
}

Because no read follows a write inside the same loop, the browser batches all the writes and runs one layout pass for the whole batch. Time complexity drops from O(N × layout) to O(layout).

The general rule: in any function that touches geometry, all reads come first, then all writes.

If you cannot decouple the reads from the writes, cache the width at component mount and recompute only when the parent actually changes size.

Properties that trigger forced layout

Any property that reports computed geometry forces a layout flush when read. The comprehensive list includes:

offsetWidth, offsetHeight, offsetTop, offsetLeft, offsetParent, clientWidth, clientHeight, clientTop, clientLeft, scrollWidth, scrollHeight, scrollTop, scrollLeft, getBoundingClientRect(), getComputedStyle(), scrollIntoView(), focus() (sometimes).

Debug this
log
[Violation] Forced reflow while executing JavaScript took 42 ms
  at applyRowWidths (list.js:88)
  at handleResize (list.js:34)
  at window.onresize (list.js:12)

[Violation] Forced reflow while executing JavaScript took 47 ms
  at applyRowWidths (list.js:88)
  at handleResize (list.js:34)
  at window.onresize (list.js:12)

[Violation] Forced reflow while executing JavaScript took 51 ms
  at applyRowWidths (list.js:88)
  ...

Three identical 'Forced reflow' warnings, all pointing at list.js:88 inside a resize handler. What pattern caused this and what is the surgical fix?

Eliminate forced sync layout in a resize handler

1/3
Quiz

A scroll handler reads `element.offsetTop` and then writes a new style. The browser pauses. What just happened?

Quiz

You wrap a read-then-write loop in `requestAnimationFrame`. Does this fix layout thrash?

Recall before you leave
  1. 01
    What is forced synchronous layout (layout thrash)?
  2. 02
    Why does wrapping a read-then-write loop in rAF not fix layout thrash?
  3. 03
    Name five properties whose read triggers a forced layout flush.
Recap

Forced synchronous layout (layout thrash) occurs when JS reads a computed-geometry property after writing a style. The browser must flush all pending writes and run a full layout pass to answer the read. In a loop over N elements this runs N full layouts — a 5000-row list at 1 ms/layout blocks the main thread for 5 seconds. The fix is a two-pass batch: collect all reads into an array in pass one, then apply all writes in pass two. The browser flushes layout once for the entire batch. Wrapping the loop in rAF does not fix the problem — rAF moves the invocation boundary, not the invalidation boundary.

Connected lessons
appears again in143
Continue the climb ↑BeginMainFrame, compositor-driven animations, and GPU memory
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.