@tenphi/tasty 0.0.0-snapshot.27b6708 → 0.0.0-snapshot.28eb83c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +38 -54
  2. package/dist/{babel-BUQGeOXA.d.ts → babel-D6lOuSwk.d.ts} +2 -2
  3. package/dist/chunks/async-storage-DKK-wTD4.js.map +1 -0
  4. package/dist/chunks/build-config-BBThSdlo.js +45 -0
  5. package/dist/chunks/build-config-BBThSdlo.js.map +1 -0
  6. package/dist/{collector-DTahQUiV.js → chunks/collector-DcViW2ZQ.js} +28 -14
  7. package/dist/chunks/collector-DcViW2ZQ.js.map +1 -0
  8. package/dist/chunks/config-engine-DQVQK4NU.js +403 -0
  9. package/dist/chunks/config-engine-DQVQK4NU.js.map +1 -0
  10. package/dist/chunks/css-definitions-BZ9vm0ci.js +1284 -0
  11. package/dist/chunks/css-definitions-BZ9vm0ci.js.map +1 -0
  12. package/dist/chunks/css-resources-Cyl_axbI.js +149 -0
  13. package/dist/chunks/css-resources-Cyl_axbI.js.map +1 -0
  14. package/dist/chunks/debug-Vyauml7U.js +583 -0
  15. package/dist/chunks/debug-Vyauml7U.js.map +1 -0
  16. package/dist/chunks/dsl-CXvoaLnj.js +2097 -0
  17. package/dist/chunks/dsl-CXvoaLnj.js.map +1 -0
  18. package/dist/chunks/hydration-CLdVKcH2.js +59 -0
  19. package/dist/chunks/hydration-CLdVKcH2.js.map +1 -0
  20. package/dist/chunks/react-runtime-so_X_yMo.js +1526 -0
  21. package/dist/chunks/react-runtime-so_X_yMo.js.map +1 -0
  22. package/dist/chunks/runtime-engine-DQ5laRou.js +3056 -0
  23. package/dist/chunks/runtime-engine-DQ5laRou.js.map +1 -0
  24. package/dist/{merge-styles-oklji0KB.js → chunks/shared-utils-OIg0N_dp.js} +42 -3
  25. package/dist/chunks/shared-utils-OIg0N_dp.js.map +1 -0
  26. package/dist/{config-B5kHzuNz.js → chunks/style-engine-CBuOt1e4.js} +2387 -7442
  27. package/dist/chunks/style-engine-CBuOt1e4.js.map +1 -0
  28. package/dist/{css-writer-B-J87ncv.js → chunks/zero-engine-BFh7gKbK.js} +3 -3
  29. package/dist/chunks/zero-engine-BFh7gKbK.js.map +1 -0
  30. package/dist/{collector-DUaHCcTS.d.ts → collector-CAmndLXe.d.ts} +23 -3
  31. package/dist/{config-YsxGv4tq.d.ts → config-BWYrv_Aa.d.ts} +141 -37
  32. package/dist/core/index.d.ts +5 -5
  33. package/dist/core/index.js +9 -6
  34. package/dist/{index-Cd45t5NM.d.ts → index-BzAC3p06.d.ts} +118 -23
  35. package/dist/{index-PqN-DIpn.d.ts → index-D4kRLj2o.d.ts} +128 -68
  36. package/dist/index.d.ts +5 -5
  37. package/dist/index.js +10 -922
  38. package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-BFktNP8J.d.ts} +2 -2
  39. package/dist/ssr/astro-client.js +1 -1
  40. package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
  41. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  42. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  43. package/dist/ssr/astro-middleware-extract.d.ts +11 -0
  44. package/dist/ssr/astro-middleware-extract.js +9 -0
  45. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  46. package/dist/ssr/astro-middleware-static.d.ts +3 -1
  47. package/dist/ssr/astro-middleware.d.ts +3 -1
  48. package/dist/ssr/astro.d.ts +45 -3
  49. package/dist/ssr/astro.js +157 -14
  50. package/dist/ssr/astro.js.map +1 -1
  51. package/dist/ssr/index.d.ts +7 -7
  52. package/dist/ssr/index.js +4 -4
  53. package/dist/ssr/index.js.map +1 -1
  54. package/dist/ssr/next-config.d.ts +66 -0
  55. package/dist/ssr/next-config.js +135 -0
  56. package/dist/ssr/next-config.js.map +1 -0
  57. package/dist/ssr/next.d.ts +9 -2
  58. package/dist/ssr/next.js +26 -10
  59. package/dist/ssr/next.js.map +1 -1
  60. package/dist/static/index.d.ts +2 -2
  61. package/dist/static/index.js +1 -1
  62. package/dist/zero/babel.d.ts +1 -1
  63. package/dist/zero/babel.js +25 -24
  64. package/dist/zero/babel.js.map +1 -1
  65. package/dist/zero/index.d.ts +1 -1
  66. package/dist/zero/index.js +1 -1
  67. package/dist/zero/next.d.ts +1 -1
  68. package/docs/README.md +5 -3
  69. package/docs/adoption.md +3 -3
  70. package/docs/ai-agents.md +83 -78
  71. package/docs/comparison.md +12 -12
  72. package/docs/configuration.md +47 -47
  73. package/docs/debug.md +19 -7
  74. package/docs/dsl.md +3 -3
  75. package/docs/getting-started.md +10 -10
  76. package/docs/injector.md +62 -25
  77. package/docs/methodology.md +3 -3
  78. package/docs/migration-v3.md +49 -49
  79. package/docs/plugins.md +37 -33
  80. package/docs/react-api.md +2 -2
  81. package/docs/runtime-benchmarks.md +351 -0
  82. package/docs/ssr.md +219 -61
  83. package/docs/styles.md +43 -11
  84. package/docs/tasty-static.md +136 -103
  85. package/package.json +80 -11
  86. package/dist/async-storage-DKK-wTD4.js.map +0 -1
  87. package/dist/collector-DTahQUiV.js.map +0 -1
  88. package/dist/config-B5kHzuNz.js.map +0 -1
  89. package/dist/context-CA8YKeMn.js +0 -24
  90. package/dist/context-CA8YKeMn.js.map +0 -1
  91. package/dist/core-Dr4u1NVD.js +0 -1566
  92. package/dist/core-Dr4u1NVD.js.map +0 -1
  93. package/dist/css-writer-B-J87ncv.js.map +0 -1
  94. package/dist/format-global-rules-DklyaXv-.js +0 -22
  95. package/dist/format-global-rules-DklyaXv-.js.map +0 -1
  96. package/dist/format-rules-DKOA-6qu.js +0 -130
  97. package/dist/format-rules-DKOA-6qu.js.map +0 -1
  98. package/dist/hydrate-OeMX99We.js +0 -37
  99. package/dist/hydrate-OeMX99We.js.map +0 -1
  100. package/dist/index.js.map +0 -1
  101. package/dist/keyframes-CV8azJf3.js +0 -493
  102. package/dist/keyframes-CV8azJf3.js.map +0 -1
  103. package/dist/merge-styles-oklji0KB.js.map +0 -1
  104. package/dist/resolve-recipes-DTG81rzl.js +0 -144
  105. package/dist/resolve-recipes-DTG81rzl.js.map +0 -1
  106. /package/dist/{async-storage-DKK-wTD4.js → chunks/async-storage-DKK-wTD4.js} +0 -0
@@ -0,0 +1,351 @@
1
+ # Runtime Benchmarks
2
+
3
+ Tasty keeps its performance claims in reproducible benchmarks rather than
4
+ combining unlike measurements into one score. The repository measures five
5
+ different costs:
6
+
7
+ 1. Style parsing and generation in Node.
8
+ 2. The React overhead of an empty `tasty({})` wrapper.
9
+ 3. Cold browser generation and injection compared with equivalent CSS that is
10
+ already on the page.
11
+ 4. The steady-state interaction path — mod flips and styled subtrees opening
12
+ and closing after the page has loaded.
13
+ 5. Page-load cold start: network, module compilation, execution and first
14
+ paint, end to end in a throttled browser.
15
+
16
+ The first four are focused microbenchmarks, not page-level scores. The fifth is
17
+ a page-level measurement and is the only one that answers "what does a visitor
18
+ wait for". Run them several times on an otherwise idle machine and use a
19
+ production profile to decide whether any cost matters in an application.
20
+
21
+ ## Reproducing the Results
22
+
23
+ ```bash
24
+ pnpm bench
25
+ pnpm bench:overhead
26
+ pnpm bench:injection
27
+ pnpm bench:interaction
28
+ pnpm bench:cold-start
29
+ ```
30
+
31
+ `pnpm bench` runs the core pipeline benchmarks in Node. The rest use production
32
+ code paths in headless Chromium. The first checkout may require
33
+ `pnpm test:setup` to download Chromium, and `pnpm bench:cold-start` needs a
34
+ current `dist/` — run `pnpm build` first.
35
+
36
+ Run the Node and browser suites separately so they do not compete for CPU. The
37
+ browser timer has 0.1 ms resolution, so the browser benchmarks perform many
38
+ matched operations per sample and divide the absolute difference by the number
39
+ of elements, rules or interactions. Machine load, browser versions, and CPU
40
+ power will move the results — on a loaded machine the absolute columns drift
41
+ several percent while the raw/Tasty delta holds, so read the delta.
42
+
43
+ ## Core Style Pipeline
44
+
45
+ The following numbers are single-call throughput measured on an Apple M1 Max
46
+ with Node 22:
47
+
48
+ | Operation | ops/sec | Latency (mean) |
49
+ | ----------------------------------------------------------- | -------------------: | -------------: |
50
+ | `renderStyles` — 5 flat properties (cold) | ~60,000 | ~17 us |
51
+ | `renderStyles` — state map with media/hover/modifier (cold) | ~18,500 | ~54 us |
52
+ | `renderStyles` — same styles (cached) | ~5,800,000 | ~0.17 us |
53
+ | `parseStateKey` — simple key like `:hover` (cold) | ~790,000 | ~1.3 us |
54
+ | `parseStateKey` — complex OR/AND/NOT key (cold) | ~140,000 | ~7 us |
55
+ | `parseStateKey` — any key (cached) | ~3,400,000–8,300,000 | ~0.1–0.3 us |
56
+ | `parseStyle` — value tokens like `2x 4x` (cold) | ~344,000 | ~2.9 us |
57
+ | `parseStyle` — color tokens (cold) | ~567,000 | ~1.8 us |
58
+ | `parseStyle` — any value (cached) | ~15,250,000 | ~0.07 us |
59
+
60
+ “Cold” cases use unique inputs to bypass the relevant caches. Cached cases
61
+ reuse one input and measure the LRU hot path. Expect roughly ±10% between runs.
62
+ These benchmarks do not include React, DOM work, stylesheet injection, style
63
+ resolution, layout, or paint.
64
+
65
+ The benchmark sources are colocated with the code they exercise:
66
+ [`pipeline.bench.ts`](../src/pipeline/pipeline.bench.ts),
67
+ [`parseStateKey.bench.ts`](../src/pipeline/parseStateKey.bench.ts), and the
68
+ parser benchmark files under [`src/parser`](../src/parser).
69
+
70
+ ## Empty Wrapper Overhead
71
+
72
+ Skipping the style pipeline does not make a `tasty()` component free. Even
73
+ `tasty({})` is a React component between its parent and the host element. React
74
+ tracks another fiber, and Tasty still processes and forwards the element's
75
+ props.
76
+
77
+ [`tasty-overhead.bench.tsx`](../src/tasty-overhead.bench.tsx) compares 10,000
78
+ raw `<div className>` siblings with 10,000 instances of one module-scoped
79
+ `tasty({})` component. Both receive the same props, and the benchmark fails if
80
+ they do not produce equivalent DOM.
81
+
82
+ The benchmark uses production React in headless Chromium. The factory is
83
+ created and its empty class-name cache is warmed before timing, so factory
84
+ creation, style generation, and injection are excluded. A detached container
85
+ excludes layout, paint, and stylesheet matching. Every commit is wrapped in
86
+ `flushSync`, keeping its synchronous reconciliation and commit inside the
87
+ sample. This does not estimate React's concurrent scheduling latency.
88
+
89
+ On an Apple M3 Pro with React 19.2.4 and Chromium 151, three consecutive runs
90
+ produced these ranges:
91
+
92
+ | Work on 10,000 siblings | Raw elements | `tasty({})` | Extra per wrapped element |
93
+ | ----------------------------------- | -----------: | -----------: | ------------------------: |
94
+ | Mount + remove | 4.5–4.9 ms | 14.0–14.9 ms | 0.95–1.00 us |
95
+ | Rerender, same host props | 1.3–1.4 ms | 10.9–11.4 ms | 0.96–1.00 us |
96
+ | Rerender, change one host attribute | 2.8–3.4 ms | 15.0–15.7 ms | 1.21–1.27 us |
97
+
98
+ The useful result is the raw/Tasty time difference divided by 10,000, not the
99
+ ratio between the two times. The ratio becomes large because the raw baseline
100
+ is tiny. In this synthetic workload, an empty wrapper adds roughly 1 us per
101
+ participating element, or 1.2–1.3 us when React also changes a DOM attribute.
102
+
103
+ This is the floor Tasty consumes when it has no styling job. It is not a
104
+ page-level score. Real trees include application components, effects, layout,
105
+ paint, and usually far fewer simultaneous styled-element updates. The benchmark
106
+ also does not measure retained memory; that requires a matched-tree heap
107
+ snapshot experiment with controlled garbage collection.
108
+
109
+ ## Cold Generation and Injection
110
+
111
+ [`tasty-injection.bench.ts`](../src/tasty-injection.bench.ts) measures the extra
112
+ work when Tasty must generate and inject CSS that an otherwise equivalent page
113
+ already has. It does not compare different stylesheet insertion techniques.
114
+
115
+ The benchmark covers two useful workloads:
116
+
117
+ - **One new rule:** add one rule to an existing stylesheet, append its one
118
+ element, and immediately read its computed style. Each timed sample performs
119
+ 50 independent one-rule transactions and reports their total; dividing the
120
+ raw/Tasty difference by 50 produces a stable per-rule result despite
121
+ Chromium's 0.1 ms timer resolution.
122
+ - **1,000 new rules together:** generate and insert all 1,000 rules into one
123
+ stylesheet, append all 1,000 elements, then read every computed color without
124
+ another write in between. This gives the browser one style-resolution
125
+ boundary for the group.
126
+
127
+ For every transaction, the existing-CSS control has the equivalent stylesheet
128
+ parsed, adopted, and attached before timing. The runtime root has a Tasty
129
+ stylesheet pre-created with an unrelated sentinel rule, but not the measured
130
+ rules. Both paths perform the same class assignment, DOM commit, and
131
+ computed-style reads. Only the runtime path calls `computeStyles()` and inserts
132
+ the new rules.
133
+
134
+ Preparation and cleanup happen outside the sample timer. Every runtime style
135
+ value is unique within a cycle, the relevant caches are cleared between cycles,
136
+ and a guard verifies that both paths resolve to the same color. React and the
137
+ `tasty()` wrapper are absent so their independently measured costs do not enter
138
+ the result. Pre-creating both stylesheets also excludes one-time sheet creation
139
+ and adoption from the subtraction.
140
+
141
+ On an Apple M3 Pro with Chromium 151, three consecutive runs produced these
142
+ ranges:
143
+
144
+ | Workload | CSS already present | Tasty runtime | Incremental Tasty cost |
145
+ | ---------------------------------------------------- | ------------------: | -------------: | ---------------------: |
146
+ | One new rule + immediate resolution, per transaction | 2.8–3.3 us | 110.3–113.8 us | 107.3–111.0 us |
147
+ | 1,000 new rules + one resolution | 1.86–2.06 ms | 9.01–9.98 ms | 7.13–7.92 ms |
148
+ | 1,000-rule workload, incremental cost per rule | — | — | 7.1–7.9 us |
149
+
150
+ Directly compared, injecting 1,000 rules before one resolution boundary cost
151
+ about 66–71 times as much in total as injecting one rule and resolving it—not
152
+ 1,000 times as much. Its average incremental cost per rule was about 14–16
153
+ times lower. This is the same Tasty generation and injection path in both cases;
154
+ the group amortizes fixed transaction work and lets the browser resolve all the
155
+ stylesheet writes together.
156
+
157
+ The subtraction is the meaningful result. It includes Tasty's cold style
158
+ generation, cache and injector bookkeeping, rule insertion, and any additional
159
+ style invalidation exposed by that workload's resolution boundary. It does not
160
+ pretend to isolate `insertRule()` from the system that calls it.
161
+
162
+ This is a deliberately cold workload. Reused styles resolve from cache and do
163
+ not inject another rule. Different rule complexity, DOM shape, stylesheet size,
164
+ browser, and hardware will change the number. The single-rule and 1,000-rule
165
+ results are not interchangeable: the first crosses the injection-to-resolution
166
+ boundary once per rule, while the second lets the browser resolve 1,000 writes
167
+ together. Because the same resolution pattern is present in each workload's
168
+ control, the difference answers the narrower delivery question: how much extra
169
+ work did Tasty perform when the same CSS was not already there?
170
+
171
+ ## Steady-State Interaction
172
+
173
+ The benchmarks above measure mounting and whole-tree updates. A running
174
+ application spends most of its time on neither. It flips mods — hovered,
175
+ pressed, selected, expanded — on elements whose styles never change, one
176
+ element at a time, and it mounts and unmounts small styled subtrees as menus
177
+ and dialogs open. Both paths go through the state-map and ref-counting
178
+ machinery rather than the parser, so a regression in them is invisible to every
179
+ other benchmark here.
180
+
181
+ [`tasty-interaction.bench.tsx`](../src/tasty-interaction.bench.tsx) pairs each
182
+ case with a raw-DOM equivalent driven by a hand-written stylesheet that
183
+ produces the same computed color and background in both states. The benchmark
184
+ fails if either arm resolves to anything else, so an arm that quietly rendered
185
+ unstyled elements cannot report a flattering number.
186
+
187
+ Each leaf owns its own `useState`, which is what keeps a single-element
188
+ interaction single: re-rendering the root to flip one row would time the whole
189
+ tree. One toggle is far below Chromium's 0.1 ms timer resolution, so a sample
190
+ flips a 100-element tree three times over — 300 commits — and the churn case
191
+ performs 20 open/close cycles. Divide the raw/Tasty difference by those counts.
192
+
193
+ Two things had to be sized deliberately, and both are the difference between a
194
+ readable number and noise:
195
+
196
+ - **The tree is small (100 elements), not large.** React locates a leaf's
197
+ pending update by walking the sibling list, so in a 1,000-element tree a
198
+ single-element update costs ~68 us of traversal — identical in both arms and
199
+ an order of magnitude above anything the styling layer contributes.
200
+ - **The sample resolves style once, not once per flip.** Forcing a recalc
201
+ between flips costs ~70 us in both arms, which buries the delta the same way.
202
+ The browser's side of an interaction is real, but it is the browser's;
203
+ resolution boundaries are what the injection benchmark above measures.
204
+
205
+ The contract check also reads the injected CSS **before** any toggle and fails
206
+ if the hovered rule is not already there. That a style map's states all ship in
207
+ one chunk on first render is the premise of this case; if the hovered rule
208
+ arrived lazily, the first sample would be timing injection.
209
+
210
+ On an Apple M1 Max with React 19.2.8 and Chromium 151, across several runs:
211
+
212
+ | Workload | Raw elements | Tasty mods | Extra per unit |
213
+ | ---------------------------------------------------- | -----------: | -----------: | -----------------------: |
214
+ | 300 single-element mod toggles in a 100-element tree | 2.1–2.5 ms | 2.7–3.1 ms | 1.6–2.1 us / interaction |
215
+ | 20 mount + unmount cycles of a 200-element subtree | 7.6–8.0 ms | 12.6–12.7 ms | 1.22–1.27 us / element |
216
+
217
+ The absolute columns move several percent with machine load; the delta between
218
+ the arms is the stable quantity, so read that rather than either column.
219
+
220
+ Two things are worth reading out of this.
221
+
222
+ A mod flip on an already-mounted element costs about 2 us. The CSS for both
223
+ states already exists — Tasty emits every state of a style map in one chunk on
224
+ first render — so both arms perform the same commit, and what is left is
225
+ Tasty's props and mod handling. That is the same order as the ~1 us empty
226
+ wrapper measured above, which is most of where it comes from.
227
+
228
+ Subtree churn is not about styling at all. Its ~1.25 us per element sits right
229
+ on the empty-wrapper mount cost, because the styles are already cached:
230
+ reopening a menu re-pays the React wrapper, not the style pipeline.
231
+
232
+ ## Page-Load Cold Start
233
+
234
+ Every benchmark above deliberately excludes the network, module compilation and
235
+ the first render. [`scripts/cold-start`](../scripts/cold-start) measures exactly
236
+ those: what a visitor waits for between requesting a page and seeing styled
237
+ content, in a real Chromium under CDP network and CPU throttling.
238
+
239
+ Three pages render the same 50 styled components and are verified, before any
240
+ timing, to produce the same 50 elements at the same computed color:
241
+
242
+ - **baseline** — the components server-rendered: identical markup, identical
243
+ class names, a linked stylesheet, and no Tasty on the page. Every other
244
+ column is a delta against this one.
245
+ - **runtime** — Tasty generates the CSS in the browser, as a client-rendered
246
+ application does. The run asserts it really did (69 rules generated).
247
+ - **prewarm** — the same, after one throwaway `computeStyles()` against a
248
+ detached root before the first component renders.
249
+
250
+ Each cell is the median of 5 uncached loads in a fresh browser context. The run
251
+ ends at the first contentful paint, observed through a `PerformanceObserver`
252
+ rather than counted in animation frames — `requestAnimationFrame` fires before
253
+ paint, so a page that commits fast can reach its second frame with nothing
254
+ painted yet.
255
+
256
+ Two things about the payload decide whether this measures a deployment or a
257
+ straw man, so both are enforced rather than assumed:
258
+
259
+ - **Assets are served brotli-compressed**, the way a static host serves them.
260
+ The bundle is 52.0 KB on the wire and 186 KB decoded; putting the decoded
261
+ bytes on a 1.6 Mbps link would add ~700 ms and charge it to Tasty. The run
262
+ reads `encodedBodySize` back out of resource timing and fails if what
263
+ crossed the wire is not the compressed size the table reports.
264
+ - **The bundle is built from what the page imports** (`tasty`, `configure`,
265
+ `computeStyles`, `tastyDebug`), so it is tree-shaken as an application's
266
+ would be. Re-exporting the whole library adds ~4 KB brotli of code no page
267
+ here calls.
268
+
269
+ On an Apple M1 Max with React 19.2.8 and Chromium 151, first contentful paint:
270
+
271
+ | Link / CPU | baseline | runtime | prewarm | Tasty's cost |
272
+ | --------------------- | -------: | ------: | ------: | -----------: |
273
+ | No throttling, 1x | 40 ms | 52 ms | 52 ms | +12 ms |
274
+ | Fast 4G, 1x | 624 ms | 680 ms | 676 ms | +56 ms |
275
+ | Slow 4G, 1x | 2028 ms | 2304 ms | 2304 ms | +276 ms |
276
+ | No throttling, 4x CPU | 148 ms | 196 ms | 196 ms | +48 ms |
277
+ | Fast 4G, 4x CPU | 684 ms | 784 ms | 788 ms | +100 ms |
278
+ | Slow 4G, 4x CPU | 2096 ms | 2416 ms | 2408 ms | +320 ms |
279
+
280
+ That is one full run of the matrix; a second moved every cell by a few percent.
281
+
282
+ **The cost is the bundle, not the work.** On Slow 4G the extra transfer alone
283
+ accounts for 262 ms of the 276 ms FCP delta — nearly all of it. Everything
284
+ Tasty then *does* is small by comparison:
285
+
286
+ | Phase (Slow 4G, 1x) | baseline | runtime | prewarm |
287
+ | ----------------------- | -------: | ------: | ------: |
288
+ | js+css transfer | 1420 ms | 1682 ms | 1681 ms |
289
+ | module compile (shared) | 1.2 ms | 1.5 ms | 2.0 ms |
290
+ | tasty top-level execute | — | 1.2 ms | 0.9 ms |
291
+ | `configure()` | — | 0.6 ms | 0.5 ms |
292
+ | prewarm | — | — | 5.3 ms |
293
+ | render 1st component | 2.0 ms | 8.2 ms | 2.7 ms |
294
+ | render 49 more | 1.0 ms | 7.1 ms | 6.0 ms |
295
+
296
+ Importing Tasty costs about 1 ms of top-level execution; `configure()` costs
297
+ half of one. The rest of the CPU delta — about 13 ms for 50 components — is
298
+ generation and injection, which is the cost the injection benchmark isolates.
299
+
300
+ One asymmetry is worth naming: the control links a render-blocking stylesheet
301
+ and the runtime modes have none, so the control's first paint waits for CSS the
302
+ runtime modes never request. That is the real difference between the two
303
+ delivery models, not a thumb on the scale, but it means the FCP delta is not
304
+ purely "what Tasty costs to execute".
305
+
306
+ **Prewarming moves the wake-up, it does not remove it.** The first styled render
307
+ is ~5 ms more expensive than the ones after it, because that is when the
308
+ engine's deferred payload is actually compiled. A throwaway `computeStyles()`
309
+ against a detached root pays it early: `render 1st` drops from 8.2 ms to
310
+ 2.7 ms. The prewarm itself costs 5.3 ms, so FCP does not move. It is worth
311
+ doing only when something else can overlap it, or when the first render is on a
312
+ latency-critical path and the page has idle time before it.
313
+
314
+ **Retained heap.** After a forced collection, the runtime page holds about
315
+ 1,013 KB more than the control (2,632 KB vs 1,619 KB) for 50 components — the
316
+ parser caches, the chunk cache, the injector's registry and the generated CSS.
317
+ The control is not zero either; most of its 1.6 MB is React and the DOM.
318
+
319
+ CPU throttling changes which line moves. At 4x, module compilation of the
320
+ larger graph becomes visible (5.9 ms → 25 ms) where at 1x it is free: V8
321
+ pre-parses at import and compiles lazily, so a slower CPU pays for code the
322
+ faster one never fully compiled. Transfer numbers from the unthrottled cells
323
+ are not worth reading — with no emulated link, resource timings are scheduling
324
+ jitter.
325
+
326
+ ## Reading the Results Together
327
+
328
+ Do not add the microbenchmark numbers together to estimate an application
329
+ blindly. They describe different paths:
330
+
331
+ - A stable `tasty()` factory can skip the style pipeline on later renders, but
332
+ its React wrapper still participates in reconciliation.
333
+ - A cached style avoids cold parsing and generation and does not insert a new
334
+ rule.
335
+ - A genuinely new style pays generation and injection once, then becomes
336
+ reusable.
337
+ - A mod flip on an already-styled element pays neither; it is a class-name
338
+ change.
339
+ - Browser style resolution, layout, and paint depend on the actual document and
340
+ need application-level profiling.
341
+
342
+ The cold-start measurement is the one that puts the rest in proportion. On a
343
+ slow connection, nearly all of Tasty's page-load cost is transferring the
344
+ library — 262 ms of a 276 ms delta — while the generation and injection the
345
+ microbenchmarks obsess over is ~13 ms for 50 components. Bundle size is
346
+ therefore the lever with the largest effect on first paint, and the runtime
347
+ levers matter for what happens after it.
348
+
349
+ The practical optimization target is therefore repeated work: keep style input
350
+ stable when possible, reuse generated chunks, and generate CSS at build or
351
+ server time when runtime flexibility is unnecessary.