Measured Results

Chrome for Testing 149.0.7827.55 (arm64 macOS), viewport 1200×800, via playwright-core/CDP. Style and layout timings come from Performance.getMetrics (RecalcStyleDuration, LayoutDuration) — the renderer's own accounting, not a performance.now() wrapper, which cannot see work the browser does after your JavaScript returns.

Reproduce: cd ../src && npm run all.


1. Containment — 3,000 cards × 12 rows (261,011 DOM nodes)

variantDOM 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

content-visibility: auto cut initial layout by 98% — 401.9 ms → 7.9 ms — with an identical node count, no JavaScript, and one CSS declaration.

contain: strict did essentially nothing here (398.4 vs 401.9 ms), and that is the important half of the result. Containment does not skip rendering; it bounds propagation. Everything was still onscreen-or-not-yet-known and therefore still rendered. A team that reaches for contain expecting content-visibility's effect measures no improvement and concludes containment is useless.


2. Invalidation scope — 500 cards, 60 mutations inside one of them

variantstyle recalclayoutlayout count
no containment0.5 ms12.7 ms120
contain: layout0.5 ms10.5 ms (−18%)120
contain: strict + fixed height1.5 ms2.5 ms (−80%)120

Same mutation, same DOM, same number of forced layouts. The only variable is how far the browser is permitted to let the change travel.

contain: layout alone gives 18% because the box can still change size, so following siblings may still move. Adding contain: size (via strict) plus a fixed height removes that too — the box cannot resize, so nothing outside it can be affected, and the layout collapses to the card's own subtree.

Note the small style-recalc increase under strict (0.5 → 1.5 ms): containment is not free, it adds bookkeeping. It pays here by a factor of five, and would not pay on a small subtree.


3. Cascade layers vs specificity

casewinnerwhy
specificity only, no layersRED (1,1,0)highest specificity wins, as expected
@layer reset, base, app — app rule 0,1,0, base rule 1,1,0BLUE (0,1,0)later layer wins; specificity never consulted
unlayered .btn vs layered #widget .btn.primaryBLUE (unlayered)unlayered styles form an implicit final layer
!important in an earlier layerRED (earlier layer)author !important reverses layer order

Resolution order confirmed (first difference decides):

  1. origin + importance — author !important reverses layer order
  2. layer order — later layer wins, beats specificity entirely
  3. specificity
  4. source order

The fourth row is the trap. A design system that ships defaults with !important becomes unoverridable by applications, which is the exact opposite of the intent. Worth an explicit rule in any system's contribution guide.


Harness notes

  • Performance.getMetrics is cumulative, so every measurement is a delta around the operation, with requestAnimationFrame×2 plus a forced offsetHeight to make the pipeline actually run before sampling. Without the settle step you measure whatever happened to have completed.
  • Fresh page per variant, for the reasons established in fe-02 and fe-03.
  • The invalidation experiment forces layout synchronously on every mutation (document.body.offsetHeight) so that layout count is identical across variants — 120 in every row. Without that, the browser would coalesce differently per variant and the comparison would be meaningless.
  • A mechanical trap, twice now: escaped apostrophes inside single-quoted console.log strings in these scripts, and backticks inside page template literals in fe-03. Both produce syntax errors reported at a confusing line.