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 |
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.
Related Guides
- Forced Synchronous Layouts β the parent guide on why geometry reads flush layout mid-task.
- How to batch DOM reads and writes to prevent thrashing β the full batching,
requestAnimationFrame, andResizeObserverplaybook. - Observing Long Tasks with PerformanceObserver β catch the same regression in the field.
- Layout and Paint Optimization β the broader pipeline stage this guide sits under.