Step 2 — How Far Does One Change Travel?
Goal
Measure invalidation scope, and understand containment as a promise you make to the engine.
Predict first
500 cards. Sixty times, we insert a row into card #10, force layout, and remove it. Three
variants: no containment, contain: layout, contain: strict + fixed height.
Layout is forced synchronously every time, so the layout count is identical in all three. Predict the layout time for each.
Run
npm run invalidation
Expected output:
variant | style recalc | layout | layout count
no containment | 0.5ms | 12.7ms | 120
contain: layout | 0.5ms | 10.5ms | 120
contain: strict + fixed height | 1.5ms | 2.5ms | 120
What just happened
Same mutation, same DOM, same 120 forced layouts. Only the permitted propagation differed.
contain: layout alone gave 18% — because the card can still change size, so following
siblings may still move. The containment promise was partial.
Adding contain: size (via strict) plus a fixed height gave 80% — the box cannot resize, so
nothing outside it can be affected, and layout collapses to the card's own subtree.
Note the style-recalc increase: 0.5 → 1.5 ms. Containment is not free; it adds bookkeeping. It paid 5× here and would not pay on a small subtree. You should be able to say when it would not.
The promise is also a constraint
contain: size means the box's size does not depend on its contents. If content must grow, it is
clipped or collapses to zero. The optimisation and the correctness constraint are the same
declaration, which is why containment belongs in code review rather than a stylesheet-wide sweep.
Why the experiment forces layout deliberately
Calling document.body.offsetHeight after every mutation is the fe-01 sin, committed on purpose: it
pins layout count so the comparison measures layout cost. Without it the browser coalesces
differently per variant and the numbers are not comparable.
General rule: when comparing cost, hold the count fixed. When comparing strategies, measure both.
Checkpoint
docs/verification.md Checkpoint 3.