Analysis

Choosing a layout mode

Since the measured spread is 1.4×, choose for expressiveness:

NeedModeWhy
Two-dimensional relationships, alignment across rows and columnsgridthe only mode that expresses both axes; subgrid extends it to children
One-dimensional distribution, unknown item countflexgap, flex-wrap, and content-based sizing without magic numbers
Document flow, proseblockcheapest, and what the content actually is
Overlay, tooltip, precise placement relative to a containing blockabsolutebut measure — it was slowest here, with 2.4× the style cost
Legacy compatibility onlyfloatslowest on both axes; no reason to choose it in new code

The performance-shaped question that is actually worth asking is not "which mode" but "how many boxes am I asking the browser to lay out, and how often?" That is fe-05's territory (content-visibility, containment) and fe-01's (avoiding forced synchronous layout).

Why absolute positioning measured slowest

Two contributions, and separating them matters:

  1. Style cost, 27.7 ms vs ~11 ms. Every item carried inline left/top. Inline styles bypass selector matching but still require value computation per element, and they defeat the shared computed-style caching that identical elements otherwise benefit from.
  2. Layout is not avoided. Out-of-flow boxes still need their containing block resolved, their own sizing, and their own paint order. "Out of flow" means "does not affect siblings", which helps invalidation scope — visible in the relayout column (9.8 ms, better than block's 23.5 ms) — not initial cost.

The lesson generalises: a technique can be worse on one axis and better on another, and folklore usually remembers only one of them.

Container queries: capability, cost, constraint

Capability. A component can respond to the space it is given. This is what makes a design-system component honest — the same card in a 320 px sidebar and a 900 px main column can differ without the consuming page passing a prop describing where it is.

Cost. +1.4 ms layout, +3.0 ms style over 8,000 elements. Negligible at any realistic component count.

Constraint — the part that matters. container-type: inline-size implies size containment on the inline axis: the container's inline size may not depend on its contents. Consequences:

  • A container whose width is determined by its content (a fit-content wrapper, a table cell, an inline-block sized to text) cannot be a query container without changing its sizing behaviour.
  • container-type: size (both axes) is stricter still and will collapse height unless one is set.
  • Query containers cannot query themselves — styles inside @container apply to descendants, and a container cannot restyle its own width based on its own width. That circularity is why the containment requirement exists.

Practical rule: put container-type on a wrapper you control that has an externally-determined width, and query it from the component inside. Do not put it on the component root and then try to size that root from its contents.

When media queries remain correct

Container queries are not a replacement:

DecisionQuery
How many columns in the page shell@media
Whether the primary nav collapses@media
Print styles@media print
prefers-reduced-motion, prefers-color-scheme, forced-colors@media (user preference, not size)
Whether a card shows a horizontal or stacked layout@container
Whether a data table becomes a definition list@container

The distinction: page-level composition is a viewport question; component internals are a container question. A codebase that uses only media queries cannot build reusable components; one that uses only container queries cannot express its page shell.

Intrinsic sizing in practice

min-content is the expensive keyword (1.6×) and also the most commonly needed one — it is what prevents a flex item collapsing below its content. The idiom worth knowing:

.item { min-width: 0; }            /* opts OUT of the default min-content floor */
.item { min-width: min-content; }  /* opts back in explicitly */

Flex and grid items default to min-width: auto, which resolves to min-content — the single most common cause of "my flex item won't shrink" and of unexpected overflow in grid. Setting min-width: 0 is the fix, and it is also a small performance win because it removes a measurement.

fit-content measured within 7% of a fixed width and removes most magic numbers; prefer it over hardcoded widths where the content should decide.

What breaks at scale

  • Subgrid support assumptions. subgrid is the correct tool for aligning nested content across cards, and its absence is usually worked around with fixed heights that break under long strings or large text settings (fe-26). Check support before designing around it.
  • aspect-ratio and CLS. Reserving space for media before it loads is the cheapest CLS fix available, and it is a layout decision made here rather than a performance patch later (fe-11).
  • Container query units in shared components. cqw inside a component makes it depend on being inside a query container. If a consumer forgets container-type, units resolve against the nearest ancestor that is one — or the small-viewport fallback — silently. Design-system components should either declare their own container or document the requirement loudly.
  • Stacking contexts created accidentally. Every transform, opacity < 1, filter, and contain: paint creates one; z-index then only competes within it. The commonest version is a performance change (will-change, contain) breaking an overlay (fe-05).
  • Layout thrash is orthogonal to all of this. fe-01 measured 98× from interleaved reads and writes. No layout mode choice recovers that.