Concepts — CSS Architecture, Containment & Invalidation
Phase 1 · Platform substrate · Specification area §4 (cascade, inheritance, specificity, cascade
layers, custom properties, containment, content-visibility, design tokens, theming) plus the CSS
strategy comparison. Feeds §11 (performance), §26 (design systems), §38.
1. What is it
CSS is two things engineers routinely conflate:
- A resolution algorithm — the cascade decides which declaration wins, using origin, importance, layer, specificity and source order, in that priority.
- An invalidation system — every change tells the browser to redo some amount of style recalculation, layout, paint and compositing. How much is determined by what you have promised it about your boxes.
Most CSS architecture debate is about the first. Most CSS performance is the second. A Principal Engineer needs both, and needs to know which one a given problem is.
2. Why it matters
Rendering cost is a property of your CSS, not your DOM size. Measured here: an identical
261,011-node DOM took 401.9 ms of initial layout with no containment and 7.9 ms with
content-visibility: auto — a 98% reduction with no nodes removed and no JavaScript. The DOM was
never the problem; the instruction to the browser was.
Invalidation scope is a design decision. Mutating one card among 500 cost 12.7 ms of layout
unbounded, 10.5 ms with contain: layout, and 2.5 ms (80% less) with contain: strict. Same
mutation, same DOM, same forced-layout count — only the promise differed.
Cascade layers eliminate a whole class of organisational conflict. Measured: a layered rule with
specificity 0,1,0 beats an unlayered-priority rule at 1,1,0, because layer order is evaluated
before specificity. That is what lets a design system ship defaults an application can override
with ordinary selectors — no specificity inflation, no !important war, no :where() tricks.
This is where design systems succeed or fail (fe-38). Token architecture, theming and override strategy are all cascade decisions, and they are extremely expensive to change once dozens of teams depend on them.
3. How it works
+==================================================================+
| THE CASCADE — first difference decides |
+==================================================================+
1. ORIGIN + IMPORTANCE
transition > !important(UA > user > author) > animation
> author > user > UA
^ author !important REVERSES layer order (measured)
2. LAYER ORDER <- later layer wins, BEATS specificity entirely
unlayered styles form an implicit FINAL layer (measured)
3. SPECIFICITY (id, class, type)
4. SOURCE ORDER
+==================================================================+
| INVALIDATION — what one change forces |
+==================================================================+
change a value
|
+-- inherited property? -> invalidate descendants
+-- affects box size? -> LAYOUT (and possibly siblings, ancestors)
+-- affects pixels only? -> PAINT
+-- transform/opacity? -> COMPOSITE only (off main thread)
|
v
containment BOUNDS how far this can travel:
contain: layout internal layout cannot affect outside
contain: paint descendants never paint outside the box
contain: size box size does not depend on contents
contain: style counters/quotes cannot escape
contain: strict = layout + paint + size + style
content-visibility: auto -> skip rendering work entirely while offscreen
Measured
| DOM nodes | initial style | initial layout | |
|---|---|---|---|
| no containment | 261,011 | 99.5 ms | 401.9 ms |
content-visibility: auto | 261,011 | 10.9 ms | 7.9 ms |
contain: strict | 261,011 | 89.6 ms | 398.4 ms |
| mutation inside one of 500 cards | layout time | vs none |
|---|---|---|
| no containment | 12.7 ms | — |
contain: layout | 10.5 ms | −18% |
contain: strict + fixed height | 2.5 ms | −80% |
contain and content-visibility solve different problems. contain: strict barely helped
initial rendering (398 ms vs 402 ms) because everything was still being rendered — it bounds
propagation. content-visibility: auto skips the work entirely for offscreen subtrees. Reaching for
the wrong one produces no improvement and a confident belief that "containment doesn't work".
4. Core terminology
| Term | Definition |
|---|---|
| Cascade | The algorithm selecting a winning declaration: origin/importance → layer → specificity → source order |
| Cascade layer | @layer group; later layers win regardless of specificity |
| Unlayered styles | Form an implicit final layer — they beat every explicit layer |
| Specificity | (id, class, type) triple; only consulted after layer order |
:where() | Zero-specificity wrapper; the pre-layers technique for low-priority defaults |
| Custom property | --x; inherited, resolved at computed-value time, dynamic at runtime |
| Containment | Promise that limits how far a change can propagate |
content-visibility: auto | Skip rendering for offscreen subtrees; implies contain: layout style paint |
contain-intrinsic-size | Placeholder size for skipped subtrees; prevents scrollbar jumping |
| Style recalculation | Matching selectors and computing values |
| Layout / reflow | Computing geometry |
| Stacking context | Isolated z-ordering scope, created by transform, opacity < 1, will-change, etc. |
| Logical property | Flow-relative (margin-inline-start) rather than physical; correct under RTL (fe-26) |
5. Mental models
CSS is a set of promises to the rendering engine. contain: layout says "nothing inside this
box can change layout outside it." The browser gives you 80% less layout work in exchange for that
promise. Every containment property is this bargain, and breaking the promise (a child that must
overflow) breaks the layout, not just the optimisation.
Cascade layers are an ownership model, not a feature. @layer reset, base, components, utilities
is a statement about which team's CSS wins by default. Getting it right removes the need for
specificity games permanently; getting it wrong is very expensive to change once shipped.
Custom properties are runtime, everything else is build time. A Sass variable is gone after
compilation. A custom property is inherited, live, overridable per subtree, and readable from JS.
That is why theming, dark mode and per-component overrides use custom properties and cannot use
preprocessor variables — but it also means they cost more, and it is why you cannot transition them
without @property.
Reach for content-visibility before virtualization. Measured: 98% less initial layout for one
CSS declaration, with no JS, no scroll handlers, no measurement code, and no accessibility risk.
Virtualization (fe-30) is powerful and expensive; try the declarative option first.
6. Common misconceptions
-
"Large DOM is inherently slow." Measured false. 261,011 nodes rendered in 7.9 ms of layout with
content-visibility: autoand 401.9 ms without. The node count was identical. -
"
containmakes things faster." It bounds propagation. Measured:contain: strictgave no meaningful initial-render improvement, and 80% on the update path. Wrong tool → no result → wrong conclusion. -
"Specificity determines the winner." Only within a layer. Measured:
0,1,0in a later layer beat1,1,0in an earlier one. And unlayered styles beat everything layered. -
"
!importantis always last resort but at least it's predictable." Measured: author!importantreverses layer order, so the earliest layer wins. A design system's!importantdefeats the application's. -
"Custom properties are just variables." They are inherited and resolved at computed-value time, so a change repaints every subtree that inherits them. Their power and cost are the same property.
-
"CSS-in-JS is slower, full stop." The runtime-vs-build-time distinction matters more than the syntax. Zero-runtime extraction and plain CSS Modules land in the same place; the debate is ergonomics, bundling and dynamic theming (see
docs/analysis.md).
7. Interview talking points
- "Large DOM isn't inherently slow — we measured 261,000 nodes at 402 ms of layout without
containment and 7.9 ms with
content-visibility: auto. Same nodes. The cost was what we'd told the browser it was allowed to skip." - "
containandcontent-visibilitysolve different problems and teams conflate them.containbounds propagation — 80% off our update path. It did essentially nothing for initial render, because everything was still being rendered." - "Cascade layers are an ownership model. Layer order beats specificity entirely, so a design system
can ship defaults in an early layer and applications override with ordinary selectors. It removes
the
!importantwar structurally rather than by convention." - "The catch is
!importantinverts layer order — so a design system using!importantfor its defaults becomes unoverridable. That's worth an explicit rule in the system's contribution guide." - "Before reaching for virtualization I try
content-visibility. One declaration, no scroll handlers, no measurement code, no accessibility risk — and in our measurement a 98% cut in initial layout."
8. Connections to other modules
fe-01— forced synchronous layout (98× measured there) is the same pipeline; this module is about scoping it, that one about ordering it.fe-06(layout) — intrinsic sizing and container queries decide when containment is possible;contain: sizeandcontain-intrinsic-sizeare the bridge.fe-11(performance) — style and layout are thepresentationterm of INP.fe-26(i18n) — logical properties are the difference between an RTL-capable stylesheet and a rewrite.fe-30(large-scale UI) —content-visibilityis the declarative alternative to virtualization, and the comparison is measured there.fe-38(design systems) — layer architecture, token strategy and override policy are the system's most expensive-to-reverse decisions.browser-framework-internals.md§10 (CSS Engine), §12 (Layout Engine) — cross when you need invalidation implementation rather than behaviour.