The bench
Synthetic nodes styled three ways on this page, on this machine, in this browser. Median of 3 runs. The point is not to win — it is to stop arguing from priors.
| nodes | lane | first paint (ms) | restyle all (ms) |
|---|---|---|---|
| 100 | CSS <style> | n/a | n/a |
| 100 | DSSL live | n/a | n/a |
| 100 | DSSL frozen | n/a | n/a |
| 1,000 | CSS <style> | n/a | n/a |
| 1,000 | DSSL live | n/a | n/a |
| 1,000 | DSSL frozen | n/a | n/a |
| 10,000 | CSS <style> | n/a | n/a |
| 10,000 | DSSL live | n/a | n/a |
| 10,000 | DSSL frozen | n/a | n/a |
How to read this. First paint = build every node AND fully apply its styling (CSS injects a stylesheet, DSSL runs its first matching pass). Restyle all = flip one knob so every node changes color: for CSS a container class, for DSSL the signal bridge. The frozen lane restyles through an explicit signal call — a developer action, by design — while its ambient cost (scroll, hover bookkeeping, mutations) is exactly zero after freeze(), which the green line above reports. Numbers are floor-subtracted: each lane is also measured on an empty host with the same flow, and that wait-only floor comes off every figure, so what you see is work.
A frozen DSSL page keeps its resolved inline styles forever; unfreeze() re-arms everything. Live DSSL buys back that cost in what it can do: tokens, signals and states without a build step. Pick the lane per page, not per project.