Finding Layout Thrashing in DevTools

Layout thrashing leaves an unmistakable signature in the Chrome DevTools Performance panel: purple Layout events flagged with red Forced reflow warnings, attributed through the call tree to the exact JavaScript line that read geometry. This guide walks the trace from capture to attribution to fix. It builds on Forced Synchronous Layouts, part of Layout and Paint Optimization.

Capturing a Usable Trace

A forced reflow that disappears at native CPU speed still drops frames on a mid-tier phone, so always throttle.

  1. Performance panel β†’ enable Screenshots and set CPU to 6Γ— slowdown.
  2. Click record, reproduce the janky interaction (scroll, accordion toggle, list update), then stop.
  3. Zoom the main-thread track to the long task β€” the one with a red corner ribbon marking it as Long task (> 50ms).
Capture workflow for a usable layout-thrash trace Four ordered steps: throttle the CPU, record, reproduce the janky interaction, then zoom to the long task. Record under throttling, then isolate the long task Throttle CPU 6x slowdown Record screenshots on Reproduce jank scroll / toggle Zoom to long task red ribbon > 50ms

The Visual Signature

Inside the long task, layout work is colored purple. Two markers confirm thrashing:

  • A purple Layout (or Recalculate Style) block with a small red triangle in its top-right corner.
  • Hovering it shows the warning: β€œForced reflow is a likely performance bottleneck” along with the total time spent in forced layout for that task.
Annotated main-thread track inside one long task A long task bar contains a function call, a recalculate-style block and a purple layout block carrying a warning triangle for forced reflow. Main thread β€” one 21.3ms task Long task (red corner ribbon > 50ms budget overrun) Function Call β€” renderRows Recalculate Style Layout (forced reflow) warning triangle = forced synchronous layout
[Main Thread]  Task 21.3ms  β–Έ red ribbon: Long task
β”œβ”€ Function Call  renderRows  (19.0ms)
β”‚  β”œβ”€ Recalculate Style  (3.2ms)   ⚠
β”‚  └─ Layout  (11.8ms)             ⚠ Forced reflow β€” likely bottleneck
β”‚     └─ get offsetHeight @ rows.js:42  ← attributed read
└─ Paint  (1.4ms)
Frame Budget: 16.6ms | Actual: 21.3ms β€” DROPPED

The aggregated β€œRecalculate Style / Layout” warning in the Summary tab gives you the total forced-layout time across the whole task β€” the number to drive toward zero.

Call-Tree Attribution

Select the warned Layout event, then open the Bottom-Up or Call Tree tab. The bottom-up view roots the layout cost at the DOM API that forced it β€” get offsetHeight, getBoundingClientRect, get scrollTop β€” and the Source link jumps straight to the offending line. This is the fastest way to find which read triggered the flush when the loop body is buried in framework code.

Bottom-up attribution from layout cost to source line The warned layout event resolves down through the geometry getter to the exact source line that forced the flush. Bottom-Up view roots the cost at the read Layout β€” 11.8ms (warned) get offsetHeight (geometry read) Source: rows.js:42

If the read sits inside a long-running JavaScript task, pair this trace with Observing Long Tasks with PerformanceObserver to catch the same regression in the field, where you cannot open DevTools.

Reproduction

This loop forces one layout per row because it reads offsetHeight right after writing a class:

// ❌ Reproduces forced reflow: read-after-write inside a loop
function renderRows(rows) {
  for (const row of rows) {
    row.classList.add('measured')   // write: dirties layout tree
    const h = row.offsetHeight      // read: forces synchronous layout (rows.js:42)
    row.dataset.h = h               // write: dirties again
  }
}

Run it over a few hundred rows under 6Γ— throttling and the trace shows a stack of red-flagged Layout events whose durations sum to the warned total.

Interleaved read-after-write forces one layout per iteration Each loop iteration writes a class, reads offsetHeight, and writes again, so every read triggers its own synchronous layout flush. Iteration timeline β€” one flush per read write read β†’ flush write write read β†’ flush iteration 1 iteration 2 N rows β†’ N synchronous layout flushes

The Fix

Separate the phases so all writes land first, then all reads resolve in a single flush.

// βœ… One flush for every read; no forced reflow markers in the trace
function renderRows(rows) {
  rows.forEach((row) => row.classList.add('measured')) // write phase
  const heights = rows.map((row) => row.offsetHeight)  // single layout flush
  rows.forEach((row, i) => { row.dataset.h = heights[i] }) // write phase
}
Batched phases collapse to a single layout flush All writes run first, then all reads resolve together, producing one layout flush for the whole batch. Phase-separated timeline β€” one flush total write phase all classList.add read phase single layout flush write phase all dataset.h N rows β†’ 1 layout flush, no forced-reflow markers

Re-record the same interaction. The purple Layout block shrinks to one event with no red triangle, and the Recalculate Style / Layout summary warning vanishes. The detailed batching strategies β€” including requestAnimationFrame deferral and ResizeObserver β€” are in How to batch DOM reads and writes to prevent thrashing.

Verification Checklist

Metric Target How measured
Forced reflow warnings in task 0 Performance panel hover / Summary
Total forced layout time 0ms Recalculate Style / Layout summary
Layout events per interaction 1 Main-thread track, purple blocks
Task duration < 50ms (no Long task ribbon) Main-thread track

Reading the Warning Annotation

Chrome’s Performance panel does most of the detective work for you once you know where to look. A forced reflow shows up as a Layout event nested inside a JavaScript call frame β€” not at the frame boundary where a normal layout runs β€” and the timeline marks it with a red-cornered β€œForced reflow” warning. Clicking that warning jumps straight to the line of code that read a geometry property mid-task, which is usually the fastest possible path from symptom to cause. The bottom-up and call-tree views then let you aggregate how much total time the forced layouts cost across the recording, which is the number that tells you whether the thrash is a minor inefficiency or the dominant cost of the interaction.

The second signal worth training your eye on is the pattern of alternating events. A healthy frame shows a single Layout at the end; a thrashing frame shows Layout, Recalculate Style, Layout, Recalculate Style repeating within one task, each pair triggered by a read that followed a write. When you see that sawtooth, the fix is always the same shape β€” gather the reads before the writes β€” and re-recording after the change should collapse the sawtooth into a single trailing layout. Confirming that collapse in a fresh trace is what turns β€œI think I fixed it” into β€œthe forced reflows are gone,” and it takes only as long as a second recording.

Frequently Asked Questions

Why does the forced reflow warning appear on Layout but not on my JavaScript function?

DevTools attributes the cost to the pipeline phase that actually ran, which is Layout, and flags it because a script forced that phase to run synchronously mid-task. The JavaScript line that triggered it is one level down: select the warned Layout event and open the Bottom-Up tab to root the cost at the geometry getter such as get offsetHeight, then follow the Source link to the exact line.

Do I need CPU throttling to see layout thrashing in the trace?

You need it to make the cost visible and reproducible. At native desktop speed a forced synchronous layout can finish in well under a millisecond and never trip the Long task ribbon, yet the same read still drops frames on a mid-tier phone. Setting CPU to 6x slowdown widens the purple Layout blocks so the red warning triangle and the summed forced-layout time become obvious.

What is the difference between Recalculate Style and Layout in the trace?

Recalculate Style resolves the computed style for dirtied elements; Layout (reflow) computes their geometry. A geometry read like getBoundingClientRect forces both to flush synchronously, which is why you often see a purple Recalculate Style block immediately followed by a warned Layout block inside the same task. The Recalculate Style / Layout summary aggregates the total forced time to drive toward zero.

Can I catch the same regression in production without opening DevTools?

Yes. A PerformanceObserver watching long-animation-frame entries reports the same long tasks in the field, and the LoAF entry names the attributed script URL. Pair it with Observing Long Tasks with PerformanceObserver to alert when a forced-reflow regression ships.