Engrain by ByteRush

WEB.01.04 Mid-level· 10 minCore — asked often

Forced synchronous layout and layout thrashing

Why reading offsetHeight right after changing a style forces the browser to lay out at once, how a loop of reads and writes thrashes, and how to fix it by batching reads before writes.

Comes up in: Web fundamentals, Coding, Debugging

Learn#

Imran’s product grid freezes for half a second whenever the window resizes. His code sets each card’s height to match its width:

for (const card of cards) {
  const width = card.offsetWidth;          // read: needs fresh layout
  card.style.height = `${width * 0.75}px`; // write: makes layout dirty again
}

Writing a style does not lay out the page at once; the browser marks layout as dirty and waits for the next frame. But reading geometry, such as offsetWidth or getBoundingClientRect(), needs a correct answer now. So the browser runs layout immediately: a forced synchronous layout.

Alternating reads and writes in a loop forces a layout on every pass. That is layout thrashing. The fix: read everything first, then write everything.

Read, write, read, write: one layout per loopStep 1 of 4

State

  • layout: dirty

Loop 1: write card[0].style.height. The layout is now dirty; the browser plans to redo it before the next frame.

Read the steps as text
  1. Loop 1: write card[0].style.height. The layout is now dirty; the browser plans to redo it before the next frame.
  2. Loop 2: read card[1].offsetWidth. The answer depends on the dirty layout, so the browser lays out the page right now.
  3. Write again, read again: another forced layout. With 500 cards, that is 500 layouts in one frame.
  4. Batched: read all 500 widths first (one layout), then write all 500 heights. The next frame lays out once more.

Predict#

Predict first

for (const el of items) { el.style.width = '100px' }. Then one read of items[0].offsetHeight. How many layouts does this force?

Interview angle#

Say it like this — about 30 seconds

“Changing a style marks layout as dirty, and the browser normally recalculates once, before the next frame. Reading geometry such as offsetWidth or getBoundingClientRect needs current values, so it forces layout right away. In a loop that alternates writes and reads, every read forces a full layout: that is layout thrashing. I batch all reads first, then all writes, or schedule writes in requestAnimationFrame. The Performance panel marks forced reflows with a warning.”

Stays on your device. Nothing is uploaded.
Follow-up What is layout thrashing and how do you fix it?
Alternating style writes and geometry reads, which forces a layout on each read. Batch reads, then writes.
Follow-up Which properties force layout when read?
Geometry and scroll values: offsetWidth, clientHeight, scrollTop, getBoundingClientRect(), getComputedStyle() for layout values, and innerText.
Follow-up How do you avoid reading geometry at all?
Use CSS for the layout (aspect-ratio, grid), or ResizeObserver, which reports sizes after layout without forcing one.

Practice#

Guided

Build a small read/write scheduler, the idea behind the library fastdom. The tests use a fake page that counts forced layouts.

Done when

  • flush runs every queued measure before any mutate.
  • A mutate queued during a measure still runs in the same flush, after all measures.
  • The fake page reports at most one forced layout when you batch 100 cards.
Your solution
Ctrl/⌘ + Enter

Remember#

Try to answer each card before you open it. Then add them to your reviews.

What is a forced synchronous layout?

Layout run immediately because code read geometry while layout was dirty.

What is layout thrashing?

Alternating style writes and geometry reads, forcing a layout on every read.

How do you fix layout thrashing?

Do all reads first, then all writes.

Name three reads that force layout.

offsetWidth, getBoundingClientRect(), scrollTop.

Go deeper

Four layers. Stop at the one your interview needs; each ends with a quick check.

The core ideaEvery round

Writes are cheap; they only mark work for later. A read forces layout only when layout is dirty, so many reads in a row cost one layout.

Check yourself: Does writing many styles in a row force layouts?

No. Writes only mark layout dirty; reads force it.

How it works underneathSenior rounds

requestAnimationFrame runs just before the frame’s own style and layout (JS.01.04). Do reads first in your task, then make writes in a rAF callback, so they land in the layout the browser does anyway.
Paul Irish keeps a list of what forces layout Source: Paul Irish: What forces layout / reflow.

Check yourself: Why put DOM writes in requestAnimationFrame?

They then run just before the frame's own style and layout, which happen once anyway.

Edge cases and trapsWhere people slip

Thrashing can cross components: one component writes, another reads in the same task. Frameworks batch their own DOM writes, but a useLayoutEffect that reads geometry after another wrote can still force layout.

Check yourself: Can a third-party script cause thrashing in your code?

Yes. Any read after any write, from any script, forces layout.

In productionStaff-level answers

In the Performance panel, forced layouts appear as purple Layout bars inside your JavaScript, with a “Forced reflow” warning and the line that caused it Source: web.dev: Avoid large, complex layouts and layout thrashing.
Long forced layouts hurt INP (QLT.01.01).

Check yourself: What does the Performance panel show for a forced reflow?

A purple Layout event with a forced reflow warning, linked to the line of JavaScript.

Finished the five steps? Mark it, and its cards join your reviews.

Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

Interview questions this answers

  • What is layout thrashing and how do you fix it?