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.
- Performance panel β enable Screenshots and set CPU to 6Γ slowdown.
- Click record, reproduce the janky interaction (scroll, accordion toggle, list update), then stop.
- Zoom the main-thread track to the long task β the one with a red corner ribbon marking it as Long task (> 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.
[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.
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.
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
}
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 |
- A
long-animation-frame