Part 4 · 11 chapters · ~80 min

Layout

Layout turns computed styles into geometry: a position and a size for every box, resolved against containing blocks, by the algorithm of whichever formatting context the box lives in. It is the least parallel stage and the one where one change propagates furthest, so this part is as much about containing relayout as about running it: the box model, block and inline layout and line breaking, the flex and grid algorithms step by step, positioning and stacking, incremental layout, forced synchronous layout, and reserving space.

44

What layout computes, and the tree it runs on

the question

"Style is done. Every element knows its width is 50%. Why is there still work?"

Because 50% of what? Layout turns computed styles, many of which are relative, into absolute geometry: an x, a y, a width and a height for every box, in a coordinate system, resolved against the containing block. It runs on the layout tree, which is built from the DOM plus computed styles and is not the same shape as either.

the layout tree versus the DOM
  1. display: none produces no layout object. The subtree is absent.
  2. ::before and ::after produce layout objects with no DOM node.
  3. Anonymous boxes are inserted: text directly inside a block gets an anonymous block wrapper; a table without tbody gets one; a flex container's bare text gets an anonymous flex item.
  4. display: contents removes the element's own box and promotes its children, so it is in the DOM and absent from layout.
  5. Inline and block are different object types with different algorithms, chosen from the computed display value.
worked numbers
what each box ends up with:

  position    (x, y) relative to its container's content box
  size        (width, height) of its border box
  overflow    the rectangle its content actually occupies
  fragments   one per line for inlines, one per column or page when fragmented

in LayoutNG these are immutable fragment objects, which is what made layout cacheable and parallel-safe.
why layout is the stage you avoid retriggering
Style is per element and parallelisable (Firefox does it on all cores). Paint is per layer. Layout is a dependency graph: a child's height depends on its content, the parent's height depends on the children, a sibling's position depends on the previous sibling's height. It is the least parallel stage and the one where one change propagates furthest. Everything in this part is about containing that propagation.
45

The box model and formatting contexts

Every box is laid out by the rules of the formatting context it participates in. The context is established by the parent, and it is the algorithm. Choosing display on a container is choosing which algorithm its children get.

ContextEstablished byLays out childrenCharacteristic
Block (BFC)Root, display: flow-root, overflow ≠ visible, floats, absolutes, flex and grid itemsVertically, full width, in orderMargins collapse between siblings and parent/child; floats are contained by a new BFC
Inline (IFC)A block containing only inline contentInto line boxes, wrappingLine height, baseline alignment, bidi, white-space handling
Flexdisplay: flex / inline-flexOne axis, grow and shrinkContent-aware sizing; order; wrapping into lines
Griddisplay: grid / inline-gridTwo axes, tracks and cellsExplicit placement; fr; subgrid; auto-placement
Tabledisplay: table and friendsRows and columns with auto or fixed algorithmColumn widths depend on all rows: whole-table dependency
Ruby, mathdisplay: ruby, mathSpecialRare
the facts about boxes that generate bugs
  1. Margin collapse. Adjacent vertical margins in a BFC merge to the larger. A child's top margin can escape through its parent if nothing separates them. A new BFC (flow-root, overflow, padding, border) stops it. Flex and grid items never collapse margins.
  2. box-sizing. content-box is the default and means width excludes padding and border. *, *::before, *::after { box-sizing: border-box } is in every reset for a reason.
  3. Percentages resolve against the containing block, and for height only if the containing block's height is definite. A percentage height inside an auto-height parent computes to auto.
  4. min-width: auto on flex and grid items means "my content's minimum size", which is why long words and wide tables refuse to shrink inside flex rows. min-width: 0 or overflow: hidden on the item.
  5. Replaced elements (img, video, canvas, iframe) have intrinsic sizes and aspect ratios. Width and height attributes on an img let layout reserve space before the bytes arrive, which is the single biggest CLS fix.
BOXES AND FORMATTING CONTEXTS
what layout is actually solving
swipe the figure sideways, or tap expand for full screen
1/6
the box
A box: content, padding, border, margin. Width and height in the default box model size the content area; border-box sizing makes them include padding and border. Margins are outside, and vertical margins between block siblings collapse.
46

Block and inline layout, and line breaking

Block layout is the easy one: stack boxes vertically. Inline layout is where the real algorithm lives, because text has to be shaped, measured and broken into lines.

inline layout, in order
  1. Collect inline content into a single run: text nodes, inline boxes, replaced elements, with bidi reordering applied.
  2. Shape the text. Characters become glyphs via the font's shaping engine (HarfBuzz in Blink and Gecko). Ligatures, kerning, complex scripts. Shaping is cached per run, which is why changing one character in a long paragraph is cheaper than it looks.
  3. Find break opportunities. Per the Unicode line breaking algorithm, plus CSS overrides: white-space, word-break, overflow-wrap, hyphens. Languages differ; CJK can break almost anywhere, Thai needs a dictionary.
  4. Fill lines greedily. Add items until the next one does not fit, break at the last opportunity, start a new line. Browsers use greedy breaking, not Knuth-Plass; text-wrap: pretty and balance run a limited lookahead on top.
  5. Size each line box. Height from line-height and the tallest inline; baseline alignment from vertical-align and font metrics (ascent, descent, which is why line-height: 1 can clip).
  6. Position fragments. Each inline box may be split across lines; each piece is a fragment with its own rectangle, which is what getClientRects() returns.
worked numbers
why inline layout cost scales with text:

  shaping       O(characters), cached by run
  breaking      O(characters), re-run when width changes
  line boxes    O(lines)

  a width change on a paragraph re-breaks every line below the change.
  a font-size change re-shapes and re-breaks everything.

long text in a container whose width animates is a layout cost per frame proportional to the text.
47

Flexbox: the algorithm

Flexbox is one-dimensional: items along a main axis, lines along a cross axis. The specification's algorithm has nine steps; four of them do the work.

the steps that matter
  1. Hypothetical main size. From flex-basis (or width/height if basis is auto, or content if both are auto), clamped by min and max.
  2. Line collection. With wrap, items are split into lines where the sum of hypothetical sizes exceeds the container.
  3. Resolve flexible lengths. Per line: free space = container minus sum minus gaps. Positive: distribute by flex-grow. Negative: distribute by flex-shrink weighted by basis. Items that would violate min or max are frozen at the limit and the remainder is redistributed. Loop until stable.
  4. Cross size. Each line is as tall as its tallest item. align-items stretches or aligns items within the line; align-content places lines in the container.
  5. Main-axis alignment. justify-content distributes any remaining space (which exists only when nothing grows).
what the algorithm explains
  1. flex: 1 means flex: 1 1 0%: basis zero, so all free space is shared equally regardless of content. flex: auto means 1 1 auto: content first, then share the rest. Different results with different content.
  2. Shrinking is weighted by basis, so the biggest item gives up the most, which is usually what you want and sometimes surprising.
  3. min-width: auto blocks shrinking below content. The overflowing flex row is this, every time.
  4. Percent heights inside flex items are late-resolved, because the line height is not known until step 4.
  5. Nested flex containers multiply passes. Each level may need two layouts (measure, then final), so deep flex nesting is a real cost on wide tables of flex rows.
THE FLEX ALGORITHM
the steps the spec actually runs
swipe the figure sideways, or tap expand for full screen
1/6
hypothetical sizes
Three items in a 600 px container with a 12 px gap. Each item's flex-basis is "auto", so the hypothetical size is its content width: 90, 180 and 140 px. Total 410 plus 24 of gaps.
48

Grid: track sizing and placement

Grid is two-dimensional and its algorithm is correspondingly larger. Track sizing resolves columns and rows; placement puts items in cells; the two interact when tracks are content-sized.

track sizing, in order
  1. Initialise. Each track gets a base size and a growth limit from its definition: fixed lengths are both; auto and min-content start at zero with growth from content; fr is skipped until step 4; minmax(a, b) sets both.
  2. Resolve intrinsic sizes. Items spanning one track contribute their min-content and max-content to that track's base and limit. Then items spanning more tracks distribute their contribution across them.
  3. Maximise. If free space remains and tracks have growth limits above their base, grow them.
  4. Expand flexible tracks. Remaining space is divided among fr tracks by weight. An fr track's minimum is its content minimum (unless minmax(0, 1fr)).
  5. Expand stretched auto tracks. If space still remains, auto tracks share it.
  6. Repeat for the other axis, using the now-known column sizes to measure item heights.
placement
  1. Explicit placement first: items with both grid-row and grid-column set, then those with one.
  2. Auto-placement walks the grid in order, row by row (or column by column), placing each item in the first cell that fits. grid-auto-flow: dense backfills earlier holes, reordering visually relative to source, which matters for accessibility.
  3. Implicit tracks are created when placement runs past the explicit grid, sized by grid-auto-rows and grid-auto-columns.
  4. Subgrid lets a nested grid adopt its parent's tracks on an axis, so nested items align to the outer grid.
cost
Grid is more expensive per container than flex because track sizing considers every item on both axes, twice. It is still fast; the thing to avoid is a grid with hundreds of content-sized rows where every item change re-runs the two-axis resolution. For long lists, a block container of fixed-height rows is cheaper than a grid with auto rows.
GRID TRACK SIZING
how fr and auto resolve
swipe the figure sideways, or tap expand for full screen
1/6
the template
grid-template-columns: 200px auto 1fr 2fr, in an 800 px container with no gaps. Four tracks, four different sizing rules.
49

Positioning, containing blocks and stacking

Position removes a box from normal flow in different ways, and each way changes what the box is sized and placed against.

the containing block, per position
  1. static, relative. The nearest block container ancestor's content box. relative then offsets from where it would have been, without affecting others.
  2. absolute. The nearest ancestor with position other than static, or one with transform, filter, perspective, contain: layout, or will-change: transform. Those last ones surprise people: a transform on a parent captures absolutely positioned descendants.
  3. fixed. The viewport, unless an ancestor has transform, filter or similar, in which case that ancestor. The "fixed header breaks inside an animated container" bug.
  4. sticky. In flow, but offset within its nearest scrolling ancestor once it crosses the threshold. The sticky box is constrained to its containing block, so a sticky inside a short parent stops early.
stacking contexts and z-index
  1. z-index only works on positioned boxes (and flex and grid items). On a static box it is ignored.
  2. A stacking context is created by a positioned box with z-index other than auto, opacity below 1, transform, filter, isolation: isolate, will-change for those properties, mix-blend-mode, contain: paint, and a few more.
  3. z-index is compared only within a stacking context. A z-index of 9999 inside a context with z-index 1 sits below a z-index 2 sibling of that context. This is the whole explanation for every "my modal is behind the header" bug.
  4. Paint order within a context: background, negative z, blocks, floats, inlines, z-index 0 and auto positioned, positive z. Part 5 draws it.
the lens
DevTools' Layers panel (Part 5) shows compositing layers; for stacking, the Elements panel's computed z-index plus a walk up the ancestors looking for anything that creates a context is the method. When z-index does not work, the answer is always an ancestor, never a bigger number.
50

Overflow, scrolling containers and fragmentation

overflow
  1. visible is the default: content spills, and the overflow rectangle is tracked for hit testing and painting.
  2. hidden, clip, scroll, auto clip to the padding box. hidden and auto make the box a scroll container; clip does not, which is cheaper and does not establish a new formatting context for the same reason.
  3. A scroll container gets a scrollable overflow rectangle, scrollbars (which take layout space unless overlay or scrollbar-gutter), and, on the compositor, its own scrolling layer so the main thread is not involved in scrolling it. Part 5.
  4. overflow on one axis forces the other: overflow-x: hidden with overflow-y: visible computes to auto on y. A frequent cause of unexpected scrollbars.
fragmentation
  1. Multi-column, paged media and regions split a box's content across fragmentainers. Each fragment gets its own rectangle.
  2. break-before, break-after, break-inside control where. break-inside: avoid is the one that matters for print and for multicol cards.
  3. Layout cost multiplies because content may be laid out, found not to fit, and laid out again in the next fragment.
51

Layout invalidation and incremental layout

Like style, layout does not rerun for the whole tree on every change. The engine marks boxes dirty, propagates the dirt along dependency edges, and relayouts only the dirty subtree, with caching for what did not change.

what gets marked
  1. The changed box, for a change to its own size-affecting style.
  2. Its ancestors, up to the nearest box whose size cannot be affected: a box with a fixed size and contain: layout or size, or an absolutely positioned box with a definite size. Without containment, dirt propagates to the root, because a parent's height depends on its children.
  3. Its following siblings in block flow, because their positions depend on it.
  4. Its descendants, if its width changed, because percentages and wrapping depend on the container width.
what LayoutNG caches
  1. Fragment results keyed by the constraint space (available width, mode, etc.). A subtree laid out at the same width with unchanged content reuses its fragment without recomputing. This is why relayout after a change deep in one column is cheap if the column width did not change.
  2. Intrinsic sizes (min-content, max-content) per box, invalidated on content change.
containment, which is the tool
contain: layout promises the engine that nothing inside affects anything outside, so dirt stops at the boundary. contain: size promises the box's size does not depend on its contents. content-visibility: auto does both for off-screen content and skips its layout entirely until it approaches the viewport. On a long list, content-visibility: auto on each item is the single biggest layout win available, with the caveat that scrollbar size estimates need contain-intrinsic-size.
52

Forced synchronous layout and thrash

the question

"My loop sets a style and reads offsetHeight. It is 200 items. Why is it 300 ms?"

Because each read forces the engine to run layout immediately so the answer reflects the write before it. Layout that would have run once at the end of the task runs 200 times inside it. This is forced synchronous layout, and in a loop it is layout thrash.

the rule and the exceptions
  1. Reads before writes. In any task, gather every geometry value first, then apply every style change. Layout is clean when you read and dirtied once when you write.
  2. Use requestAnimationFrame for the writes if they depend on the reads and you want them to land before the next paint.
  3. Use ResizeObserver instead of polling. It delivers sizes after layout, in a batch, without forcing anything.
  4. Use IntersectionObserver instead of getBoundingClientRect in scroll handlers. Same reason.
  5. Cache what does not change. A container's width read once at the top of a loop is one layout, not n.
  6. The first read in a task is free if nothing has dirtied layout since the last frame. It is the read after a write that costs.
go to the lab
  1. Route /layout/thrash runs the write-read loop on 300 cards. Record it. Find the comb of Layout events and the red triangle on each marking "forced reflow".
  2. Toggle the "batched" variant and record again. One Layout, at the end.
  3. Route /layout/containment changes one card's text in a list of 2,000. Record with and without contain: layout on the cards and compare the Layout event's "nodes that need layout".
  4. Route /layout/content-visibility scrolls a 10,000-row list with and without content-visibility: auto. Compare total Layout time in the summary.
FORCED SYNCHRONOUS LAYOUT
read, write, read, write
swipe the figure sideways, or tap expand for full screen
1/6
normal
Normal flow. Script runs, makes some style changes, the engine marks layout dirty, and at the end of the task, before the next frame, layout runs once.
53

Intrinsic sizing, aspect ratio and reserving space

Layout shift is layout running again with different inputs after the user has started reading. The inputs that change late are sizes the engine could not know early: images, fonts, embeds, late content. Intrinsic sizing is how you tell it early.

the tools
  1. width and height attributes on img and video. The engine computes the aspect ratio from them before the file arrives and reserves the box. With CSS width: 100%; height: auto the ratio holds at any size. This alone fixes most image CLS.
  2. aspect-ratio. The same for any box: aspect-ratio: 16 / 9 on an embed container.
  3. min-height on containers that fill late. A skeleton the same height as the content.
  4. contain-intrinsic-size. For content-visibility: auto content, the placeholder size used while it is skipped.
  5. Metric-matched fallback fonts. Part 3. The swap does not reflow if the fallback has the same advance widths.
  6. Never insert above existing content unless in response to a user action. Banners, ads and "new items" bars go below or overlay.
worked numbers
the sizes layout works with:

  min-content   the narrowest the box can be without overflowing (longest word)
  max-content   the width if nothing wrapped
  fit-content   clamp(min-content, available, max-content)
  stretch       fill the container

  width: auto on a block     = stretch
  width: auto on a float     = fit-content
  width: auto on a flex item = content-based, then flexed

CLS is measured in Part 12. every entry in it traces to a size that arrived late.
54

Reading layout cost in the profile

What you seeWhat it meansWhat to do
Layout, purple, with a red corner, inside a script blockForced synchronous layoutReorder reads before writes; check the call stack in the summary for the read
A comb of many small Layout events in one taskThrash in a loopBatch; or use FastDOM-style read/write phases
One large Layout after a small changeDirt propagated to the rootAdd containment at a boundary; give containers definite sizes
Layout on every frame of an animationAnimating a layout property (width, height, top, margin)Animate transform instead; Part 5
Layout on scrollA scroll handler reading geometry, or sticky/position: fixed with layout dependenciesIntersectionObserver; passive listeners; move to the compositor
Layout Shift entries in the Experience trackCLSClick one: it names the shifted element; reserve its space
the summary pane numbers that matter
A Layout event's summary shows "Nodes that need layout" and "Layout root". The first is the dirty set; the second is where the relayout started. A layout root of #document for a change inside one card means containment is missing. Those two numbers are the whole diagnosis.