skip to content

Layout Thrashing

Knowing that interleaved reads and writes are slow is the easy half; finding the loop responsible in a real trace and refactoring it is the interview question. This node is the diagnosis and remediation workflow.

on this pageshow

questions

4

In a Chrome DevTools performance recording, one JavaScript call in the flame chart sits above dozens of alternating short "Recalculate Style" and "Layout" entries, instead of a single layout at the end of the frame. What does that pattern tell you, and how do you get from it to the line of code responsible?

level: middleimportance: must knowfreq 55%

answer

  1. repetition inside one call, not total time
  2. count the entries, count the rows
  3. layout entries carry a forced-layout marker
  4. one long layout means something else
  5. re-record to prove the collapse

basics

~20 s

That sawtooth is layout thrashing: the code measures geometry and mutates the DOM in the same loop, so each iteration forces the browser to redo layout. To locate it, open the detail pane on one of those layout entries — DevTools flags a forced layout and links to the JavaScript that triggered it.

solid answer

~50 s

Many short style-and-layout pairs stacked under a single script call means the browser was dragged through layout once per iteration rather than once per frame — the classic thrash signature. The strongest confirmation is that the number of layout entries tracks the size of the collection being processed: forty rows, roughly forty layouts. To attribute it, select one of the layout entries; DevTools marks forced layouts and its summary links back to the script that forced them, which takes you straight to the reading line. I would also use the Bottom-Up view to see how much of the frame is layout in total, so I know what fixing it is worth. Two patterns argue against thrashing: one long layout entry, which points at document size or structure instead, and a long scripting entry with no layout underneath at all, which is plain script cost.

go deeper

for a junior

Recognise that many short style-and-layout entries stacked under one function call is a distinctive pattern with a name, and know that DevTools can point from such an entry back to the script that caused it.

for a middle

Explain the sawtooth as one forced layout per iteration, tie the number of entries to the size of the collection being processed, and walk the path from the entry's detail pane to the reading line in source.

for a senior

Demonstrate that you rule alternatives out — a single long layout, a script-only long task, layout with no script above it — and that you size the win from aggregate layout time before you refactor anything.

for a principal

Own the measurement policy: decide when a lab trace on one machine is enough evidence to act, what field signal corroborates it across real hardware, and how the team avoids trading engineering time for wins that never reach users.

## The signature A browser prefers to lay out once per frame: run your JavaScript, then recalculate style, lay out, paint, composite. When a trace shows that rhythm, the rendering work appears as one block after the scripting block. Layout thrashing produces the opposite shape — a single scripting entry with many short *Recalculate Style* / *Layout* pairs nested underneath it, in a repeating sawtooth. Every pair is one round trip: something in the loop invalidated layout, and the next statement asked for a value that could not be answered without redoing it. The visual cue is the *repetition inside one call*, not the total time. Ten milliseconds of layout in one entry and ten milliseconds spread over sixty entries mean different things and have different fixes. ## Read it as a count first Before touching code, ask how many layout entries there are and what on the page is that many. If a table has forty rows and the trace shows roughly forty layout entries, you have both the diagnosis and the prediction: doubling the rows doubles the cost, so the interaction degrades linearly with data size. That is also the cheapest way to prove the fix later — the same interaction on the same data should show one layout entry, not forty. ## Getting to the call site The flame chart tells you *which* script call owns the sawtooth; the layout entries tell you *who forced them*. Select one of the layout entries and read its detail pane: DevTools flags layout that was forced by script and links back to the JavaScript responsible, so you can jump to the source line that performed the read. Chrome's console may also have printed a forced-reflow violation during the same interaction, which corroborates the finding but names no location. Two supporting views are worth the click. The **Bottom-Up** view, filtered to the interaction's time range, aggregates how much of the frame went into layout and style — that is the number that tells you whether this is worth an afternoon. The **Call Tree** view shows the path from the event handler down to the offending function, which matters when the reading line is inside a shared helper called from three places rather than in the handler you were looking at. ## What the same trace could have been instead Attribution is only useful if you can rule things out. - **One long layout entry.** Layout ran once but took a long time. That is not thrashing; it points at the size or shape of the document — a very large DOM, a deeply nested or highly interdependent structure — and the remedy is structural, not a reordering of reads and writes. - **A long scripting entry with no layout underneath.** The main thread was busy with computation, parsing, or framework work. Reordering DOM access will not move it. - **Repeated layout across many separate frames.** Each frame lays out once, which is normal; if it is still too slow, the question is why the page relayouts every frame, not whether reads and writes are interleaved. - **Layout entries with no script above them.** Something outside your code — a media element resizing, a font swapping in, an image arriving without dimensions — is invalidating layout. ## From attribution to a fix Once you have the call site, the fix is one of three shapes, in increasing order of value. Separate the passes so all measurement happens before all mutation. Cache the measurement when it cannot change during the loop — measuring a container's width once outside the loop instead of once per child often removes the whole sawtooth. Best of all, delete the measurement: if the code is only asking "is this on screen" or "has this box changed size", an observer reports that without your code ever reading geometry. ```js // one read, then N writes — the sawtooth collapses to a single layout const width = container.getBoundingClientRect().width; for (const child of container.children) { child.style.width = `${width / 2}px`; } ``` ## Prove it, then stop Re-record the same interaction and compare: the layout entries under that call should collapse to one, and total layout time in Bottom-Up should fall by roughly the factor you predicted from the count. If layout time barely moves, the sawtooth was not where the frame was going, and you have learned that before shipping a refactor nobody needed.

  • The trace shows forty layout entries, but layout accounts for only 3ms of a 200ms interaction. What do you do?
    Leave it. The sawtooth is real but it is not where the frame went — the remaining 197ms is script, and refactoring the loop buys almost nothing. This is exactly why you read the aggregate before rewriting code: the pattern being present is not the same as the pattern being expensive. I would note it, then profile the scripting entry to find what actually dominates.
  • How would you detect this pattern in the field rather than on your own machine?
    Traces are a lab tool. In the field, the Long Animation Frames API attributes work inside slow frames to individual scripts, and each script's attribution includes the time that script spent in forced style and layout. Collecting that alongside your interaction telemetry tells you whether real users hit the same forced-layout cost, and on which scripts — you lose the per-entry detail, but you gain a population instead of one laptop.
  • The sawtooth is inside a third-party widget you cannot edit. What are your options?
    You cannot fix their loop, so you change what surrounds it. Give the widget a container whose size does not depend on the page, so its measurements stay valid and cheap; avoid mutating the DOM around it in the same frame; and consider deferring or lazily mounting it so the cost never lands inside an interaction. If it remains the dominant cost, that is a vendor conversation or a replacement decision, backed by the trace.

saying these in an interview costs you the question

  • Reads total layout time and ignores the repetition
  • Assumes any purple layout entry means thrashing
  • Cannot say how to reach the call site from an entry
  • Confuses a single long layout with many short ones
  • Rewrites the loop before checking what layout costs

context

open as a page

A scroll listener on a long page calls getBoundingClientRect() on about forty elements on every scroll event, to decide which ones are on screen and to reposition a sidebar. Scrolling stutters, and batching the reads and writes only helps a little. What would you replace the measurement with, and why is that cheaper?

level: seniorimportance: must knowfreq 62%

basics

~20 s

Stop measuring on scroll. Use IntersectionObserver for on-screen decisions and ResizeObserver for size-dependent ones: the browser reports the geometry it already computed and calls you only when something crosses a threshold, so scroll frames where nothing changed cost nothing. A sticky sidebar usually needs no JavaScript at all.

open as a page

While scrolling a page, Chrome's console prints `[Violation] Forced reflow while executing JavaScript took 47ms`. What is that warning telling you about your code, and what would you do with it?

level: juniorimportance: should knowfreq 38%

basics

~20 s

Chrome prints that when your JavaScript asked the browser for a geometry value and forced it to compute layout on the spot, costing 47ms inside that script. It is a symptom, not a location: record a performance trace of the same interaction to find the call site.

open as a page

What does a DOM batching library such as FastDOM (`fastdom.measure()` / `fastdom.mutate()`) actually do to your DOM code, and why can adding it to one component still leave the page janky?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

FastDOM keeps two queues — reads and writes — and flushes them in a single animation frame with every read before every write, so a frame contains one layout instead of many. It only works if all DOM access in that frame goes through it; one unbatched reader elsewhere re-splits the frame.

open as a page