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 nodesinitial styleinitial layout
no containment261,01199.5 ms401.9 ms
content-visibility: auto261,01110.9 ms7.9 ms
contain: strict261,01189.6 ms398.4 ms
mutation inside one of 500 cardslayout timevs none
no containment12.7 ms
contain: layout10.5 ms−18%
contain: strict + fixed height2.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

TermDefinition
CascadeThe algorithm selecting a winning declaration: origin/importance → layer → specificity → source order
Cascade layer@layer group; later layers win regardless of specificity
Unlayered stylesForm 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
ContainmentPromise that limits how far a change can propagate
content-visibility: autoSkip rendering for offscreen subtrees; implies contain: layout style paint
contain-intrinsic-sizePlaceholder size for skipped subtrees; prevents scrollbar jumping
Style recalculationMatching selectors and computing values
Layout / reflowComputing geometry
Stacking contextIsolated z-ordering scope, created by transform, opacity < 1, will-change, etc.
Logical propertyFlow-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

  1. "Large DOM is inherently slow." Measured false. 261,011 nodes rendered in 7.9 ms of layout with content-visibility: auto and 401.9 ms without. The node count was identical.

  2. "contain makes things faster." It bounds propagation. Measured: contain: strict gave no meaningful initial-render improvement, and 80% on the update path. Wrong tool → no result → wrong conclusion.

  3. "Specificity determines the winner." Only within a layer. Measured: 0,1,0 in a later layer beat 1,1,0 in an earlier one. And unlayered styles beat everything layered.

  4. "!important is always last resort but at least it's predictable." Measured: author !important reverses layer order, so the earliest layer wins. A design system's !important defeats the application's.

  5. "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.

  6. "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."
  • "contain and content-visibility solve different problems and teams conflate them. contain bounds 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 !important war structurally rather than by convention."
  • "The catch is !important inverts layer order — so a design system using !important for 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: size and contain-intrinsic-size are the bridge.
  • fe-11 (performance) — style and layout are the presentation term of INP.
  • fe-26 (i18n) — logical properties are the difference between an RTL-capable stylesheet and a rewrite.
  • fe-30 (large-scale UI)content-visibility is 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.