@tenphi/tasty 0.0.0-snapshot.fdb16ea → 0.0.0-snapshot.fde6cf3

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 (87) hide show
  1. package/dist/astro-Dj8YpRd7.js +301 -0
  2. package/dist/astro-Dj8YpRd7.js.map +1 -0
  3. package/dist/{babel-CMYf4JZ2.d.ts → babel-Cbp2C2bj.d.ts} +2 -2
  4. package/dist/{collector-Ctn5s74s.d.ts → collector-DBY6liC2.d.ts} +94 -11
  5. package/dist/collector-NjmUiB8f.js +529 -0
  6. package/dist/collector-NjmUiB8f.js.map +1 -0
  7. package/dist/{config-B23gZrfP.js → config-Bd2vOF0Z.js} +454 -105
  8. package/dist/config-Bd2vOF0Z.js.map +1 -0
  9. package/dist/{config-CZx8_DCu.d.ts → config-CA7iAlEI.d.ts} +112 -22
  10. package/dist/core/index.d.ts +5 -5
  11. package/dist/core/index.js +6 -6
  12. package/dist/{core-B1tyn69d.js → core-ClInK6YG.js} +386 -107
  13. package/dist/core-ClInK6YG.js.map +1 -0
  14. package/dist/css-resources-Cyl_axbI.js +149 -0
  15. package/dist/css-resources-Cyl_axbI.js.map +1 -0
  16. package/dist/{css-writer-CvAxkR6S.js → css-writer-BDQXG9Jo.js} +3 -3
  17. package/dist/{css-writer-CvAxkR6S.js.map → css-writer-BDQXG9Jo.js.map} +1 -1
  18. package/dist/{format-rules-BCTWVvcS.js → format-rules-CGhgoVLR.js} +22 -32
  19. package/dist/format-rules-CGhgoVLR.js.map +1 -0
  20. package/dist/{hydrate-BCPvkJj3.js → hydrate-CqbxwVh_.js} +2 -2
  21. package/dist/{hydrate-BCPvkJj3.js.map → hydrate-CqbxwVh_.js.map} +1 -1
  22. package/dist/{index-B3o0aCYV.d.ts → index-C8RFYeFW.d.ts} +61 -12
  23. package/dist/{index-CsvB0EXZ.d.ts → index-FpYK0H5g.d.ts} +49 -8
  24. package/dist/index.d.ts +5 -5
  25. package/dist/index.js +43 -27
  26. package/dist/index.js.map +1 -1
  27. package/dist/{keyframes-Dxdx3KOo.js → keyframes-BjfNVhaB.js} +45 -3
  28. package/dist/keyframes-BjfNVhaB.js.map +1 -0
  29. package/dist/{merge-styles-DJTSfz_M.d.ts → merge-styles-BQv7d3fx.d.ts} +2 -2
  30. package/dist/{merge-styles-C-uwpoNW.js → merge-styles-BbJhjesV.js} +2 -2
  31. package/dist/{merge-styles-C-uwpoNW.js.map → merge-styles-BbJhjesV.js.map} +1 -1
  32. package/dist/precompile/index.d.ts +11 -0
  33. package/dist/precompile/index.js +72 -0
  34. package/dist/precompile/index.js.map +1 -0
  35. package/dist/precompile/register.d.ts +13 -0
  36. package/dist/precompile/register.js +2 -0
  37. package/dist/register-BkC-8Zuy.js +333 -0
  38. package/dist/register-BkC-8Zuy.js.map +1 -0
  39. package/dist/{resolve-recipes-BuOvSEa2.js → resolve-recipes-B2Sf36AH.js} +3 -3
  40. package/dist/{resolve-recipes-BuOvSEa2.js.map → resolve-recipes-B2Sf36AH.js.map} +1 -1
  41. package/dist/ssr/astro-client.js +1 -1
  42. package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
  43. package/dist/ssr/astro-middleware-extract-static.js +9 -0
  44. package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
  45. package/dist/ssr/astro-middleware-extract.d.ts +11 -0
  46. package/dist/ssr/astro-middleware-extract.js +9 -0
  47. package/dist/ssr/astro-middleware-extract.js.map +1 -0
  48. package/dist/ssr/astro-middleware-static.d.ts +3 -1
  49. package/dist/ssr/astro-middleware-static.js +1 -1
  50. package/dist/ssr/astro-middleware.d.ts +3 -1
  51. package/dist/ssr/astro-middleware.js +1 -1
  52. package/dist/ssr/astro.d.ts +45 -3
  53. package/dist/ssr/astro.js +1 -156
  54. package/dist/ssr/index.d.ts +2 -2
  55. package/dist/ssr/index.js +3 -3
  56. package/dist/ssr/index.js.map +1 -1
  57. package/dist/ssr/next-config.d.ts +66 -0
  58. package/dist/ssr/next-config.js +115 -0
  59. package/dist/ssr/next-config.js.map +1 -0
  60. package/dist/ssr/next.d.ts +9 -2
  61. package/dist/ssr/next.js +26 -9
  62. package/dist/ssr/next.js.map +1 -1
  63. package/dist/ssr-collector-ref-COs_ioWl.js +29 -0
  64. package/dist/ssr-collector-ref-COs_ioWl.js.map +1 -0
  65. package/dist/static/index.d.ts +2 -2
  66. package/dist/static/index.js +1 -1
  67. package/dist/types-Bo5ePLZh.d.ts +111 -0
  68. package/dist/zero/babel.d.ts +1 -1
  69. package/dist/zero/babel.js +4 -4
  70. package/dist/zero/babel.js.map +1 -1
  71. package/dist/zero/index.d.ts +1 -1
  72. package/dist/zero/index.js +1 -1
  73. package/dist/zero/next.d.ts +1 -1
  74. package/docs/README.md +3 -1
  75. package/docs/debug.md +17 -6
  76. package/docs/injector.md +23 -3
  77. package/docs/precompile.md +202 -0
  78. package/docs/runtime-benchmarks.md +143 -72
  79. package/docs/ssr.md +179 -20
  80. package/package.json +44 -10
  81. package/dist/collector-DMSR6ANK.js +0 -304
  82. package/dist/collector-DMSR6ANK.js.map +0 -1
  83. package/dist/config-B23gZrfP.js.map +0 -1
  84. package/dist/core-B1tyn69d.js.map +0 -1
  85. package/dist/format-rules-BCTWVvcS.js.map +0 -1
  86. package/dist/keyframes-Dxdx3KOo.js.map +0 -1
  87. package/dist/ssr/astro.js.map +0 -1
@@ -0,0 +1,202 @@
1
+ # Precompiled Component Catalogs
2
+
3
+ Precompiled catalogs let a design system render a curated component matrix at
4
+ build time, publish the resulting CSS as a static asset, and teach Tasty which
5
+ classes and ancillary rules that asset already contains. Covered chunks then
6
+ return their existing class names without running the style pipeline or
7
+ inserting CSS. Uncovered runtime styles continue through the normal path.
8
+
9
+ This mode complements SSR and `tastyStatic()`:
10
+
11
+ - SSR collects the styles used by one request.
12
+ - `tastyStatic()` extracts explicitly static style calls through a build
13
+ transform.
14
+ - A precompiled catalog covers the stable runtime `tasty()` styles shared by
15
+ many applications, while retaining runtime props and overrides.
16
+
17
+ ## Compile a Catalog
18
+
19
+ `@tenphi/tasty/precompile` is Node-only. Each case returns a React tree; Tasty
20
+ renders it inside the active style collector. The entry requires the optional
21
+ `react` and `react-dom` peers used by the component library.
22
+
23
+ ```tsx
24
+ import { writeFile } from 'node:fs/promises';
25
+ import { precompileTastyStyles } from '@tenphi/tasty/precompile';
26
+
27
+ import { Button, Dialog } from './index.js';
28
+
29
+ const artifact = await precompileTastyStyles({
30
+ id: '@acme/ui',
31
+ cases: [
32
+ {
33
+ id: 'button/default',
34
+ render: () => <Button>Continue</Button>,
35
+ },
36
+ {
37
+ id: 'button/danger',
38
+ render: () => <Button variant="danger">Delete</Button>,
39
+ },
40
+ {
41
+ id: 'dialog/default',
42
+ render: () => (
43
+ <Dialog open title="Account">
44
+ Content
45
+ </Dialog>
46
+ ),
47
+ },
48
+ ],
49
+ });
50
+
51
+ await Promise.all([
52
+ writeFile('dist/precompiled.css', artifact.css),
53
+ writeFile(
54
+ 'dist/precompiled.manifest.json',
55
+ JSON.stringify(artifact.manifest),
56
+ ),
57
+ writeFile('dist/precompiled.report.json', JSON.stringify(artifact.report)),
58
+ ]);
59
+ ```
60
+
61
+ Run the catalog with the same Tasty configuration and exact Tasty version that
62
+ the design system publishes. Generation rejects page-relative CSS resources
63
+ such as `url(font.woff2)` because their meaning changes when the stylesheet is
64
+ moved. Absolute, root-relative, and data URLs are accepted.
65
+
66
+ The output is deterministic for a deterministic catalog and configuration.
67
+ The report lists the classes and ordered collector artifacts first introduced
68
+ by each case; use it to remove cases that add no coverage.
69
+
70
+ The Node-only precompile entry renders each returned tree with React DOM's
71
+ `renderToStaticMarkup()` inside the collector context. Catalog cases only need
72
+ to construct their representative component tree, including any providers the
73
+ design system normally requires.
74
+
75
+ ## Publish and Register
76
+
77
+ The production default is a static stylesheet plus manifest registration:
78
+
79
+ ```ts
80
+ // @acme/ui/precompiled
81
+ import { registerTastyPrecompiled } from '@tenphi/tasty/precompile/register';
82
+ import manifest from './precompiled.manifest.json';
83
+ import './precompiled.css';
84
+
85
+ registerTastyPrecompiled(manifest);
86
+ export * from './index.js';
87
+ ```
88
+
89
+ Mark this module and the CSS asset as side-effectful in the UI package. Keep the
90
+ normal entry unchanged so adoption is explicit. Applications that link the CSS
91
+ from HTML or a framework layout must still execute the registration module
92
+ before rendering Tasty components.
93
+
94
+ A static asset is preferred because the browser can load and cache it in
95
+ parallel with JavaScript, it is available for first paint, and it requires no
96
+ CSP nonce. Environments that must bundle CSS text can use the fallback:
97
+
98
+ ```ts
99
+ import {
100
+ installTastyPrecompiled,
101
+ type TastyPrecompiledManifest,
102
+ } from '@tenphi/tasty/precompile/register';
103
+ import css from './precompiled.css?raw';
104
+ import manifestJSON from './precompiled.manifest.json';
105
+
106
+ installTastyPrecompiled({
107
+ css,
108
+ manifest: manifestJSON as TastyPrecompiledManifest,
109
+ });
110
+ ```
111
+
112
+ Call `installTastyPrecompiled()` before the first Tasty render. It appends one
113
+ `<style data-tasty-precompiled>` element, assigns `textContent` once, and does
114
+ not place the CSS in Tasty's normal rule lifecycle. An explicit nonce overrides
115
+ the nonce from `configure()`.
116
+
117
+ ## What the Artifact Contains
118
+
119
+ The stylesheet contains only collector artifacts attributed to components:
120
+
121
+ - style chunks;
122
+ - component-local and standalone `@property`, `@keyframes`, `@font-face`,
123
+ `@counter-style`, and native `@function` rules;
124
+ - properties inferred from component declarations.
125
+
126
+ It excludes `configure()` internals, `useGlobalStyles()`, and `useRawCSS()`.
127
+ Tokens, palettes, body styles, and application globals therefore remain
128
+ dynamic. The asset is a base stylesheet: dynamic globals and uncovered chunks
129
+ are appended later and can override it. It intentionally does not use
130
+ `data-tasty-ssr`, which would incorrectly tell hydration that configured
131
+ globals were already emitted.
132
+
133
+ Dependency metadata seeds browser, SSR, and RSC caches as immutable external
134
+ definitions. Tasty neither inserts duplicate ancillary rules nor adds external
135
+ classes to its garbage-collected rule set. A precompiled hit is restricted to
136
+ the document the catalog CSS lives in — a static stylesheet in the page, or the
137
+ `<style>` `installTastyPrecompiled()` appends to the global `document`. Any
138
+ other root uses the normal runtime path: a `ShadowRoot`, which cannot see
139
+ document CSS, and another `Document` such as an iframe's, which the asset was
140
+ never installed into.
141
+
142
+ Registration is global across server module graphs and idempotent for the same
143
+ manifest ID and CSS hash. Tasty rejects incompatible versions, name prefixes,
144
+ or conflicting lookup mappings in development and safely falls back to runtime
145
+ generation.
146
+
147
+ ## Catalog Policy for UI Kits
148
+
149
+ Use the smallest matrix that exposes every unique styled structure:
150
+
151
+ 1. Render one default case for every public styled component.
152
+ 2. Render every declared visual variant.
153
+ 3. Add controlled states or props only when they change Tasty input or mount a
154
+ distinct styled subtree.
155
+ 4. Use multiple items in collections and compound components only to expose
156
+ different internal styled primitives.
157
+ 5. Do not add a clear or close case when it only reuses a shared action such as
158
+ `ItemAction` and adds no class or dependency.
159
+ 6. Remove non-default cases whose report delta is empty unless a documented
160
+ structural reason requires them.
161
+ 7. Catalog every public styled component or mark it runtime-only with a reason.
162
+
163
+ Consumer changes to parser functions, units, states, handlers, recipes, chunk
164
+ assignments, `replaceTokens`, or other compilation-affecting configuration
165
+ invalidate a shared catalog. Such applications should use runtime generation or
166
+ build a project-specific artifact.
167
+
168
+ Theming does not invalidate a catalog. `configure({ tokens })` emits `:root`
169
+ custom properties, which the asset excludes, and a chunk written against
170
+ `#brand` compiles to `var(--brand-color)` whatever the current value is — so a
171
+ palette swap leaves every compiled chunk correct. `replaceTokens` is listed
172
+ above because it is the opposite: it substitutes at parse time and bakes its
173
+ value into the declaration.
174
+
175
+ Chunk lookup is exact. A shared UI-kit catalog is most useful when applications
176
+ render those components close to their cataloged styles; frequent project
177
+ overrides can leave a large static asset mostly inactive while the expensive
178
+ custom chunks still run through the pipeline. Use `tastyDebug.summary()` to
179
+ compare active runtime and precompiled classes and to inspect distinct
180
+ precompiled classes used since metrics were reset. For heavily styled
181
+ applications, prefer a project-specific catalog that renders representative
182
+ application routes and states under the application's real Tasty
183
+ configuration. Do not load both the broad shared asset and a project-specific
184
+ asset containing the same coverage.
185
+
186
+ ## Performance Verification
187
+
188
+ Catalog generation, registration, and stylesheet loading are startup work and
189
+ should stay outside render samples. `pnpm bench:injection` includes a
190
+ 1,000-rule precompiled lookup case alongside cold generation and an
191
+ already-present CSS control. It asserts up front that the case is actually
192
+ being served from the catalog, and fails the run otherwise: a class name is
193
+ `hashString(cacheKey)` whether it came from the catalog or from runtime
194
+ generation, so nothing in the name or the rendering distinguishes a hit from a
195
+ miss, and a case that quietly stopped hitting would report the runtime path
196
+ under the precompiled label. Use `pnpm bench`, `pnpm bench:overhead`, and
197
+ `pnpm bench:injection` in alternating runs on the same idle machine when
198
+ changing this path.
199
+
200
+ With no registered manifest, the registry does not allocate lookup maps and
201
+ `computeStyles()` performs only the disabled-path boolean check before
202
+ continuing through its existing cache and injection behavior.
@@ -1,19 +1,21 @@
1
1
  # Runtime Benchmarks
2
2
 
3
3
  Tasty keeps its performance claims in reproducible benchmarks rather than
4
- combining unlike measurements into one score. The repository measures four
4
+ combining unlike measurements into one score. The repository measures three
5
5
  different costs:
6
6
 
7
7
  1. Style parsing and generation in Node.
8
8
  2. The React overhead of an empty `tasty({})` wrapper.
9
9
  3. Cold browser generation and injection compared with equivalent CSS that is
10
- already on the page.
11
- 4. A representative React update that combines wrapper, cache, generation,
12
- injection, and style-resolution work.
10
+ already on the page or registered as a precompiled catalog.
11
+ 4. Page-load cold start: transfer, compile, execution, first render, and
12
+ retained heap, against the same page built without Tasty.
13
13
 
14
- These are focused microbenchmarks, not page-load or interaction scores. Run
15
- them several times on an otherwise idle machine and use a production profile
16
- to decide whether any cost matters in an application.
14
+ The first three are focused microbenchmarks, not page-load or interaction
15
+ scores. The fourth is the page-load measurement, and it is the one to read
16
+ before concluding anything about a real application. Run them several times on
17
+ an otherwise idle machine and use a production profile to decide whether any
18
+ cost matters.
17
19
 
18
20
  ## Reproducing the Results
19
21
 
@@ -21,10 +23,10 @@ to decide whether any cost matters in an application.
21
23
  pnpm bench
22
24
  pnpm bench:overhead
23
25
  pnpm bench:injection
24
- pnpm bench:tree
26
+ pnpm bench:cold-start
25
27
  ```
26
28
 
27
- `pnpm bench` runs the core pipeline benchmarks in Node. The other three commands
29
+ `pnpm bench` runs the core pipeline benchmarks in Node. The other two commands
28
30
  use production code paths in headless Chromium. The first checkout may require
29
31
  `pnpm test:setup` to download Chromium.
30
32
 
@@ -106,21 +108,17 @@ snapshot experiment with controlled garbage collection.
106
108
  work when Tasty must generate and inject CSS that an otherwise equivalent page
107
109
  already has. It does not compare different stylesheet insertion techniques.
108
110
 
109
- The benchmark covers four group sizes with one style-resolution boundary per
110
- group:
111
+ The benchmark covers two useful workloads:
111
112
 
112
- - **One new rule:** add one rule, append its element, and immediately read its
113
- computed style. Each sample performs 50 independent groups for timer
114
- stability.
115
- - **10 new rules together:** generate and insert 10 rules, append their 10
116
- elements, then read every computed color without another write. Each sample
117
- performs 10 independent groups.
118
- - **100 or 1,000 new rules together:** perform the same sequence once per
119
- sample with 100 or 1,000 rules before the resolution boundary.
120
-
121
- Repeating the smaller groups inside one sample only raises the measurement
122
- above Chromium's timer resolution. Results are divided by that repetition
123
- count, so every row below describes one group with one resolution boundary.
113
+ - **One new rule:** add one rule to an existing stylesheet, append its one
114
+ element, and immediately read its computed style. Each timed sample performs
115
+ 50 independent one-rule transactions and reports their total; dividing the
116
+ raw/Tasty difference by 50 produces a stable per-rule result despite
117
+ Chromium's 0.1 ms timer resolution.
118
+ - **1,000 new rules together:** generate and insert all 1,000 rules into one
119
+ stylesheet, append all 1,000 elements, then read every computed color without
120
+ another write in between. This gives the browser one style-resolution
121
+ boundary for the group.
124
122
 
125
123
  For every transaction, the existing-CSS control has the equivalent stylesheet
126
124
  parsed, adopted, and attached before timing. The runtime root has a Tasty
@@ -129,6 +127,13 @@ rules. Both paths perform the same class assignment, DOM commit, and
129
127
  computed-style reads. Only the runtime path calls `computeStyles()` and inserts
130
128
  the new rules.
131
129
 
130
+ The 1,000-rule workload also includes a precompiled Tasty case. Its catalog is
131
+ generated and registered, and its CSS is parsed in an isolated document,
132
+ before the timer starts. The timed work is the chunk-key lookup, class
133
+ assignment, DOM commit, and the same computed-style reads. This keeps CSS
134
+ delivery and registration out of render latency while preserving the real
135
+ runtime lookup path.
136
+
132
137
  Preparation and cleanup happen outside the sample timer. Every runtime style
133
138
  value is unique within a cycle, the relevant caches are cleared between cycles,
134
139
  and a guard verifies that both paths resolve to the same color. React and the
@@ -137,20 +142,20 @@ the result. Pre-creating both stylesheets also excludes one-time sheet creation
137
142
  and adoption from the subtraction.
138
143
 
139
144
  On an Apple M3 Pro with Chromium 151, three consecutive runs produced these
140
- normalized ranges:
145
+ ranges:
141
146
 
142
- | New rules before one resolution | Groups per sample | Incremental cost per group | Incremental cost per rule |
143
- | ------------------------------: | ----------------: | -------------------------: | ------------------------: |
144
- | 1 | 50 | 0.111–0.114 ms | 111–114 us |
145
- | 10 | 10 | 0.142–0.159 ms | 14.2–15.9 us |
146
- | 100 | 1 | 0.90–0.94 ms | 9.0–9.4 us |
147
- | 1,000 | 1 | 7.58–8.10 ms | 7.6–8.1 us |
147
+ | Workload | CSS already present | Tasty runtime | Incremental Tasty cost |
148
+ | ---------------------------------------------------- | ------------------: | -------------: | ---------------------: |
149
+ | One new rule + immediate resolution, per transaction | 2.8–3.3 us | 110.3–113.8 us | 107.3–111.0 us |
150
+ | 1,000 new rules + one resolution | 1.86–2.06 ms | 9.01–9.98 ms | 7.13–7.92 ms |
151
+ | 1,000-rule workload, incremental cost per rule | — | — | 7.1–7.9 us |
148
152
 
149
- The curve makes the fixed work visible. Delivering 1,000 rules before one
150
- resolution boundary cost roughly 67–73 times as much in total as delivering
151
- one rule and resolving it, not 1,000 times as much. The average incremental
152
- cost per rule was roughly 14–15 times lower because the group amortized fixed
153
- transaction work and let the browser resolve all stylesheet writes together.
153
+ Directly compared, injecting 1,000 rules before one resolution boundary cost
154
+ about 66–71 times as much in total as injecting one rule and resolving it—not
155
+ 1,000 times as much. Its average incremental cost per rule was about 14–16
156
+ times lower. This is the same Tasty generation and injection path in both cases;
157
+ the group amortizes fixed transaction work and lets the browser resolve all the
158
+ stylesheet writes together.
154
159
 
155
160
  The subtraction is the meaningful result. It includes Tasty's cold style
156
161
  generation, cache and injector bookkeeping, rule insertion, and any additional
@@ -159,41 +164,12 @@ pretend to isolate `insertRule()` from the system that calls it.
159
164
 
160
165
  This is a deliberately cold workload. Reused styles resolve from cache and do
161
166
  not inject another rule. Different rule complexity, DOM shape, stylesheet size,
162
- browser, and hardware will change the number. Because the same group and
163
- resolution pattern is present in each workload's control, the difference
164
- answers the narrower delivery question: how much extra work did Tasty perform
165
- when the same CSS was not already there?
166
-
167
- ## Representative React Tree
168
-
169
- [`tasty-tree.bench.tsx`](../src/tasty-tree.bench.tsx) bridges the isolated
170
- measurements with one production React update. Every path updates 1,000 host
171
- elements sharing 20 combinations of `color` and `padding`, then reads computed
172
- styles after the commit:
173
-
174
- - **CSS already present:** raw elements switch between classes whose rules were
175
- attached before timing.
176
- - **Tasty, warm:** the elements switch between two sets of Tasty styles that
177
- were generated before timing.
178
- - **Tasty, cold:** every update introduces 20 new combinations shared across
179
- the 1,000 elements.
180
-
181
- The Tasty paths use the normal component API and `TastyBatchProvider`. On an
182
- Apple M3 Pro with production React 19.2.4 and Chromium 151, three consecutive
183
- runs produced these mean ranges:
184
-
185
- | Update path | Total time for 1,000 elements |
186
- | ----------------------------------- | ----------------------------: |
187
- | CSS already present | 1.64–1.70 ms |
188
- | Tasty, shared styles already cached | 4.17–4.43 ms |
189
- | Tasty, 20 new shared styles | 4.62–4.80 ms |
190
-
191
- In this workload, the warmed Tasty path added 2.52–2.73 ms over raw elements
192
- with existing CSS. Introducing 20 new shared combinations added another
193
- 0.38–0.55 ms over the warm Tasty update. This is a synthetic update, not an
194
- application score: it excludes effects, layout reads, paint, and concurrent
195
- scheduling, while deliberately making all 1,000 elements participate in every
196
- commit.
167
+ browser, and hardware will change the number. The single-rule and 1,000-rule
168
+ results are not interchangeable: the first crosses the injection-to-resolution
169
+ boundary once per rule, while the second lets the browser resolve 1,000 writes
170
+ together. Because the same resolution pattern is present in each workload's
171
+ control, the difference answers the narrower delivery question: how much extra
172
+ work did Tasty perform when the same CSS was not already there?
197
173
 
198
174
  ## Reading the Results Together
199
175
 
@@ -206,11 +182,106 @@ They describe different paths:
206
182
  rule.
207
183
  - A genuinely new style pays generation and injection once, then becomes
208
184
  reusable.
209
- - A tree that shares a small style vocabulary pays the wrapper path per element
210
- but cold generation only per new combination.
185
+ - A registered precompiled chunk skips declaration rendering and rule
186
+ insertion, while uncovered chunks in the same component remain dynamic.
211
187
  - Browser style resolution, layout, and paint depend on the actual document and
212
188
  need application-level profiling.
213
189
 
214
190
  The practical optimization target is therefore repeated work: keep style input
215
191
  stable when possible, reuse generated chunks, and generate CSS at build or
216
192
  server time when runtime flexibility is unnecessary.
193
+
194
+ ## Page-Load Cold Start
195
+
196
+ ```bash
197
+ pnpm bench:cold-start
198
+ pnpm bench:cold-start -- --network slow-4g --cpu 4 --runs 7
199
+ pnpm bench:cold-start -- --mode baseline,precompiled --components 200
200
+ ```
201
+
202
+ [`scripts/cold-start/`](../scripts/cold-start/) loads a real page in headless
203
+ Chromium under emulated network and CPU conditions and splits the wait into the
204
+ parts a change can move: HTML, JS and CSS transfer, module-graph compilation,
205
+ each bundle's top-level execution, the first styled component, the rest of the
206
+ page, first contentful paint, and retained heap after a forced collection.
207
+
208
+ Assets are bundled and minified the way an application ships them. Measuring
209
+ `dist/` directly would report an unminified twelve-file build nobody serves.
210
+ Nothing is cached between runs, so every run is a first visit.
211
+
212
+ Four columns run on the identical page:
213
+
214
+ | mode | what it is |
215
+ | ------------- | ------------------------------------------------------------------------------------------------------ |
216
+ | `baseline` | No Tasty at all. The same markup and the same stylesheet, with hand-written class names — the control. |
217
+ | `runtime` | Tasty generating and injecting every chunk. |
218
+ | `precompiled` | A catalog covering the page, its CSS linked as a static stylesheet. |
219
+ | `prewarm` | Runtime, with one throwaway `computeStyles()` before the first render. |
220
+
221
+ **Read every number against `baseline`.** On its own, most of what the page
222
+ spends is React, the network and the DOM; the difference between a column and
223
+ the control is what Tasty costs.
224
+
225
+ Each run is verified before its numbers are reported: an untimed pass with
226
+ `devMode` on asserts that `precompiled` served every chunk from the catalog and
227
+ generated none, and that the other modes generated theirs. This is not
228
+ optional bookkeeping — a precompiled class name and a runtime one are the same
229
+ string, `hashString(cacheKey)`, so neither the markup nor the rendering
230
+ distinguishes a catalog hit from a miss, and a column that quietly stopped
231
+ hitting would report the runtime path under a precompiled label.
232
+
233
+ ### Results
234
+
235
+ Apple M1 Max, headless Chromium, 50 distinct styled components, median of 5.
236
+
237
+ Uncontended CPU, no network throttling — the CPU picture on its own:
238
+
239
+ | phase | baseline | runtime | precompiled | prewarm |
240
+ | ---------------- | --------: | --------: | ----------: | --------: |
241
+ | compile (graph) | 6.8 ms | 6.8 ms | 4.7 ms | 6.6 ms |
242
+ | react execute | 10 ms | 11 ms | 11 ms | 12 ms |
243
+ | tasty execute | — | 1.0 ms | 0.9 ms | 1.1 ms |
244
+ | catalog register | — | — | 0.3 ms | — |
245
+ | prewarm | — | — | — | 5.2 ms |
246
+ | first component | 1.8 ms | 6.7 ms | 5.0 ms | 2.7 ms |
247
+ | 49 more | 1.2 ms | 6.7 ms | 2.1 ms | 6.7 ms |
248
+ | **painted** | **41 ms** | **54 ms** | **49 ms** | **60 ms** |
249
+ | retained heap | 1,616 KB | 2,665 KB | 2,374 KB | 2,675 KB |
250
+
251
+ Slow 4G with a 4x CPU slowdown — closer to a mid-range phone:
252
+
253
+ | phase | baseline | runtime | precompiled |
254
+ | --------------------- | -----------: | -----------: | -----------: |
255
+ | html | 570 ms | 570 ms | 569 ms |
256
+ | **js + css transfer** | **1,551 ms** | **2,667 ms** | **2,712 ms** |
257
+ | compile (graph) | 5.0 ms | 5.5 ms | 5.9 ms |
258
+ | tasty execute | — | 3.8 ms | 4.1 ms |
259
+ | first component | 10 ms | 33 ms | 24 ms |
260
+ | 49 more | 5.6 ms | 29 ms | 8.3 ms |
261
+ | **painted** | **2,812 ms** | **3,979 ms** | **4,010 ms** |
262
+
263
+ ### What the numbers say
264
+
265
+ **Tasty's page-load cost is its bundle, not its execution.** On Slow 4G the
266
+ runtime column reaches paint 1,167 ms after the control, and 1,116 ms of that
267
+ is downloading 60 KB of extra JavaScript. Every CPU phase together — compile,
268
+ execute, and rendering all 50 components — is under 70 ms even with a 4x
269
+ slowdown. Optimising the style pipeline cannot move the number that dominates
270
+ this page; shipping less code can.
271
+
272
+ **The first styled component costs several times what the next one does**
273
+ (6.7 ms against roughly 0.14 ms each for the following 49, uncontended). That
274
+ is Tasty's deferred payload compiling on first use, not per-component work.
275
+ `prewarm` moves it off the first render — 6.7 ms down to 2.7 ms — but pays the
276
+ same cost earlier and finishes later overall, so it is worth doing only where
277
+ those milliseconds land somewhere nobody is waiting.
278
+
279
+ **A catalog roughly thirds the rendering work** (29 ms to 8.3 ms at 4x) and
280
+ takes about 300 KB off the retained heap, at the cost of a stylesheet to
281
+ download. On a fast connection it wins; on Slow 4G the extra CSS costs about
282
+ as much as the CPU it saves. It also does not remove the first-render spike —
283
+ 5.0 ms against 6.7 — because the code that computes a lookup key compiles
284
+ whether or not the lookup then hits.
285
+
286
+ **Retained heap is about 1 MB over the control for 50 components**, or 760 KB
287
+ with a catalog. It does not vary with network or CPU.