Analysis
Choosing a layout mode
Since the measured spread is 1.4×, choose for expressiveness:
| Need | Mode | Why |
|---|---|---|
| Two-dimensional relationships, alignment across rows and columns | grid | the only mode that expresses both axes; subgrid extends it to children |
| One-dimensional distribution, unknown item count | flex | gap, flex-wrap, and content-based sizing without magic numbers |
| Document flow, prose | block | cheapest, and what the content actually is |
| Overlay, tooltip, precise placement relative to a containing block | absolute | but measure — it was slowest here, with 2.4× the style cost |
| Legacy compatibility only | float | slowest 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:
- 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. - 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-contentwrapper, 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
@containerapply 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:
| Decision | Query |
|---|---|
| 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.
subgridis 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-ratioand 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.
cqwinside a component makes it depend on being inside a query container. If a consumer forgetscontainer-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, andcontain: paintcreates one;z-indexthen 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.