@tenphi/tasty 0.0.0-snapshot.06c8a41 → 0.0.0-snapshot.07ea4a0
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.
- package/README.md +408 -385
- package/dist/babel-types-BKDTT4--.d.ts +18 -0
- package/dist/chunks/async-storage-f9j9JRtq.js +37 -0
- package/dist/chunks/async-storage-f9j9JRtq.js.map +1 -0
- package/dist/chunks/build-config-CW4k04_V.js +60 -0
- package/dist/chunks/build-config-CW4k04_V.js.map +1 -0
- package/dist/{collector-B3OsM252.js → chunks/collector-DKWQXEwN.js} +10 -127
- package/dist/chunks/collector-DKWQXEwN.js.map +1 -0
- package/dist/chunks/config-engine-DTglOA78.js +272 -0
- package/dist/chunks/config-engine-DTglOA78.js.map +1 -0
- package/dist/chunks/config-file-DlZXJMj8.js +25 -0
- package/dist/chunks/config-file-DlZXJMj8.js.map +1 -0
- package/dist/chunks/css-definitions-_C7C-daI.js +910 -0
- package/dist/chunks/css-definitions-_C7C-daI.js.map +1 -0
- package/dist/{css-resources-Cyl_axbI.js → chunks/css-resources-P7IBUUZs.js} +2 -9
- package/dist/chunks/css-resources-P7IBUUZs.js.map +1 -0
- package/dist/chunks/debug-BapJwy9w.js +502 -0
- package/dist/chunks/debug-BapJwy9w.js.map +1 -0
- package/dist/chunks/dsl-DYSJc1-T.js +1383 -0
- package/dist/chunks/dsl-DYSJc1-T.js.map +1 -0
- package/dist/chunks/hydration-TRcz5vnk.js +41 -0
- package/dist/chunks/hydration-TRcz5vnk.js.map +1 -0
- package/dist/chunks/react-runtime-DayjnPuC.js +689 -0
- package/dist/chunks/react-runtime-DayjnPuC.js.map +1 -0
- package/dist/chunks/runtime-engine-IwGbSMb4.js +2432 -0
- package/dist/chunks/runtime-engine-IwGbSMb4.js.map +1 -0
- package/dist/chunks/runtime-state-CVrL92zX.js +152 -0
- package/dist/chunks/runtime-state-CVrL92zX.js.map +1 -0
- package/dist/{merge-styles-DuoZEsm9.js → chunks/shared-utils-y-HjvPN4.js} +60 -55
- package/dist/chunks/shared-utils-y-HjvPN4.js.map +1 -0
- package/dist/chunks/style-engine-CVmNIjow.js +5521 -0
- package/dist/chunks/style-engine-CVmNIjow.js.map +1 -0
- package/dist/{css-writer-Bgowobbx.js → chunks/zero-engine-igHhzYM6.js} +65 -145
- package/dist/chunks/zero-engine-igHhzYM6.js.map +1 -0
- package/dist/{collector-BTVXj2hS.d.ts → collector-CTU-nl0g.d.ts} +6 -5
- package/dist/{config-LfIDmVHx.d.ts → config-CcxqSZI2.d.ts} +45 -40
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +9 -6
- package/dist/hydrate-CRyjjAyl.d.ts +22 -0
- package/dist/{index-Bm9ltvdH.d.ts → index--GdqCPlp.d.ts} +271 -242
- package/dist/index.d.ts +5 -5
- package/dist/index.js +11 -922
- package/dist/{merge-styles-BcLl1u_d.d.ts → merge-styles-DSkVc6CD.d.ts} +2 -3
- package/dist/ssr/astro-client.d.ts +1 -1
- package/dist/ssr/astro-client.js +1 -14
- package/dist/ssr/astro-client.js.map +1 -1
- package/dist/ssr/astro-middleware-extract-static.d.ts +1 -2
- package/dist/ssr/astro-middleware-extract-static.js +1 -1
- package/dist/ssr/astro-middleware-extract.d.ts +1 -2
- package/dist/ssr/astro-middleware-extract.js +1 -1
- package/dist/ssr/astro-middleware-static.d.ts +1 -2
- package/dist/ssr/astro-middleware-static.js +1 -12
- package/dist/ssr/astro-middleware-static.js.map +1 -1
- package/dist/ssr/astro-middleware.d.ts +1 -2
- package/dist/ssr/astro-middleware.js +1 -13
- package/dist/ssr/astro-middleware.js.map +1 -1
- package/dist/ssr/astro.d.ts +8 -16
- package/dist/ssr/astro.js +220 -1
- package/dist/ssr/astro.js.map +1 -0
- package/dist/ssr/client.d.ts +2 -0
- package/dist/ssr/client.js +2 -0
- package/dist/ssr/index.d.ts +5 -25
- package/dist/ssr/index.js +5 -5
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/next-config.d.ts +3 -5
- package/dist/ssr/next-config.js +43 -35
- package/dist/ssr/next-config.js.map +1 -1
- package/dist/ssr/next.d.ts +3 -9
- package/dist/ssr/next.js +7 -38
- package/dist/ssr/next.js.map +1 -1
- package/dist/static/index.d.ts +8 -9
- package/dist/static/index.js +4 -22
- package/dist/static/index.js.map +1 -1
- package/dist/static/inject.d.ts +1 -2
- package/dist/{index-BmogEzTo.d.ts → types-DJEM_BOJ.d.ts} +852 -860
- package/dist/zero/babel.d.ts +67 -2
- package/dist/zero/babel.js +77 -140
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/index.d.ts +62 -8
- package/dist/zero/index.js +24 -2
- package/dist/zero/index.js.map +1 -0
- package/dist/zero/next.d.ts +4 -5
- package/dist/zero/next.js +3 -38
- package/dist/zero/next.js.map +1 -1
- package/docs/README.md +4 -1
- package/docs/adoption.md +25 -17
- package/docs/ai-agents.md +66 -33
- package/docs/comparison.md +9 -8
- package/docs/configuration.md +192 -60
- package/docs/debug.md +5 -7
- package/docs/design-system.md +117 -15
- package/docs/dsl.md +204 -82
- package/docs/getting-started.md +81 -22
- package/docs/injector.md +41 -40
- package/docs/methodology.md +48 -23
- package/docs/migration-v3.md +3 -3
- package/docs/pipeline.md +54 -27
- package/docs/plugins.md +22 -42
- package/docs/react-api.md +72 -9
- package/docs/runtime-benchmarks.md +146 -91
- package/docs/ssr.md +92 -66
- package/docs/styles.md +117 -31
- package/docs/tasty-static.md +162 -58
- package/package.json +96 -38
- package/dist/astro-CeYENy2x.js +0 -301
- package/dist/astro-CeYENy2x.js.map +0 -1
- package/dist/async-storage-DKK-wTD4.js +0 -44
- package/dist/async-storage-DKK-wTD4.js.map +0 -1
- package/dist/babel-DWCsafYw.d.ts +0 -83
- package/dist/collector-B3OsM252.js.map +0 -1
- package/dist/config-B3gPdCqd.js +0 -12421
- package/dist/config-B3gPdCqd.js.map +0 -1
- package/dist/context-CA8YKeMn.js +0 -24
- package/dist/context-CA8YKeMn.js.map +0 -1
- package/dist/core-DGm0CFHP.js +0 -1732
- package/dist/core-DGm0CFHP.js.map +0 -1
- package/dist/css-resources-Cyl_axbI.js.map +0 -1
- package/dist/css-writer-Bgowobbx.js.map +0 -1
- package/dist/format-global-rules-DklyaXv-.js +0 -22
- package/dist/format-global-rules-DklyaXv-.js.map +0 -1
- package/dist/format-rules-XRw9u7d4.js +0 -104
- package/dist/format-rules-XRw9u7d4.js.map +0 -1
- package/dist/hydrate-CNOmZprz.js +0 -37
- package/dist/hydrate-CNOmZprz.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keyframes-DE-OE76F.js +0 -535
- package/dist/keyframes-DE-OE76F.js.map +0 -1
- package/dist/merge-styles-DuoZEsm9.js.map +0 -1
- package/dist/resolve-recipes-H9NqOQuP.js +0 -144
- package/dist/resolve-recipes-H9NqOQuP.js.map +0 -1
- package/dist/ssr-collector-ref-COs_ioWl.js +0 -29
- package/dist/ssr-collector-ref-COs_ioWl.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
4
|
+
combining unlike measurements into one score. The repository measures five
|
|
5
5
|
different costs:
|
|
6
6
|
|
|
7
7
|
1. Style parsing and generation in Node.
|
|
@@ -34,37 +34,55 @@ code paths in headless Chromium. The first checkout may require
|
|
|
34
34
|
current `dist/` — run `pnpm build` first.
|
|
35
35
|
|
|
36
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
|
|
37
|
+
browser timer has 0.1 ms resolution, so the browser benchmarks perform many
|
|
38
38
|
matched operations per sample and divide the absolute difference by the number
|
|
39
|
-
of elements or
|
|
40
|
-
the results
|
|
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
|
+
**On a laptop, check the power state first.** Low Power Mode caps CPU frequency,
|
|
44
|
+
and it is the single largest source of error here — larger than background load.
|
|
45
|
+
Measured on the machine below, running on battery with Low Power Mode enabled
|
|
46
|
+
made every one of the 53 Node cases slower, by a median of 26%, and halving the
|
|
47
|
+
background load recovered only 3 points of that. A frequency cap scales every
|
|
48
|
+
case by roughly the same factor, so it does not look like noise; it looks like a
|
|
49
|
+
uniformly slower library. Verify before trusting a run:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pmset -g ps | head -2 # want 'AC Power'
|
|
53
|
+
pmset -g | grep -i lowpowermode # want 0
|
|
54
|
+
```
|
|
41
55
|
|
|
42
56
|
## Core Style Pipeline
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
| Operation |
|
|
48
|
-
| ----------------------------------------------------------- |
|
|
49
|
-
| `renderStyles` — 5 flat properties (cold) |
|
|
50
|
-
| `renderStyles` — state map with media/hover/modifier (cold) |
|
|
51
|
-
| `renderStyles` —
|
|
52
|
-
| `
|
|
53
|
-
| `parseStateKey` —
|
|
54
|
-
| `parseStateKey` —
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `parseStyle` —
|
|
58
|
+
Single-call throughput, three consecutive runs on an Apple M3 Pro with Node
|
|
59
|
+
24.18.0 on AC power:
|
|
60
|
+
|
|
61
|
+
| Operation | ops/sec | Latency (mean) |
|
|
62
|
+
| ----------------------------------------------------------- | --------------------: | -------------: |
|
|
63
|
+
| `renderStyles` — 5 flat properties (cold) | 143,000–158,000 | 6.3–7.0 us |
|
|
64
|
+
| `renderStyles` — state map with media/hover/modifier (cold) | 31,000–36,000 | 28–32 us |
|
|
65
|
+
| `renderStyles` — OR state map across contexts (cold) | 13,000–14,000 | 71–76 us |
|
|
66
|
+
| `renderStyles` — same styles (cached) | 9,830,000–10,420,000 | ~0.10 us |
|
|
67
|
+
| `parseStateKey` — simple key like `:hover` (cold) | 1,910,000–1,980,000 | ~0.50 us |
|
|
68
|
+
| `parseStateKey` — value modifier key (cold) | 1,120,000–1,160,000 | ~0.90 us |
|
|
69
|
+
| `parseStateKey` — complex OR/AND/NOT key (cold) | 341,000–357,000 | 2.8–2.9 us |
|
|
70
|
+
| `parseStateKey` — any key (cached) | 4,900,000–16,530,000 | 0.10–0.20 us |
|
|
71
|
+
| `parseStyle` — value tokens like `2x 4x` (cold) | 575,000–605,000 | ~1.7 us |
|
|
72
|
+
| `parseStyle` — color tokens (cold) | 1,160,000–1,240,000 | 0.80–0.90 us |
|
|
73
|
+
| `parseStyle` — layered functions (cold) | 175,000–184,000 | 5.4–5.7 us |
|
|
74
|
+
| `parseStyle` — any value (cached) | 29,500,000–30,750,000 | ~0.03 us |
|
|
58
75
|
|
|
59
76
|
“Cold” cases use unique inputs to bypass the relevant caches. Cached cases
|
|
60
|
-
reuse one input and measure the LRU hot path.
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
reuse one input and measure the LRU hot path. Run-to-run spread on an idle
|
|
78
|
+
machine was 1–7% for most cases, rising to 10–16% for the two `renderStyles`
|
|
79
|
+
cold cases. These benchmarks do not include React, DOM work, stylesheet
|
|
80
|
+
injection, style resolution, layout, or paint.
|
|
63
81
|
|
|
64
82
|
The benchmark sources are colocated with the code they exercise:
|
|
65
|
-
[`pipeline.bench.ts`](
|
|
66
|
-
[`parseStateKey.bench.ts`](
|
|
67
|
-
parser benchmark files under [`src/parser`](
|
|
83
|
+
[`pipeline.bench.ts`](https://github.com/tenphi/tasty/blob/main/src/pipeline/pipeline.bench.ts),
|
|
84
|
+
[`parseStateKey.bench.ts`](https://github.com/tenphi/tasty/blob/main/src/pipeline/parseStateKey.bench.ts), and the
|
|
85
|
+
parser benchmark files under [`src/parser`](https://github.com/tenphi/tasty/tree/main/src/parser).
|
|
68
86
|
|
|
69
87
|
## Empty Wrapper Overhead
|
|
70
88
|
|
|
@@ -73,7 +91,7 @@ Skipping the style pipeline does not make a `tasty()` component free. Even
|
|
|
73
91
|
tracks another fiber, and Tasty still processes and forwards the element's
|
|
74
92
|
props.
|
|
75
93
|
|
|
76
|
-
[`tasty-overhead.bench.tsx`](
|
|
94
|
+
[`tasty-overhead.bench.tsx`](https://github.com/tenphi/tasty/blob/main/src/tasty-overhead.bench.tsx) compares 10,000
|
|
77
95
|
raw `<div className>` siblings with 10,000 instances of one module-scoped
|
|
78
96
|
`tasty({})` component. Both receive the same props, and the benchmark fails if
|
|
79
97
|
they do not produce equivalent DOM.
|
|
@@ -85,19 +103,19 @@ excludes layout, paint, and stylesheet matching. Every commit is wrapped in
|
|
|
85
103
|
`flushSync`, keeping its synchronous reconciliation and commit inside the
|
|
86
104
|
sample. This does not estimate React's concurrent scheduling latency.
|
|
87
105
|
|
|
88
|
-
On an Apple M3 Pro with React 19.2.
|
|
106
|
+
On an Apple M3 Pro with React 19.2.8 and Chromium 151, three consecutive runs
|
|
89
107
|
produced these ranges:
|
|
90
108
|
|
|
91
|
-
| Work on 10,000 siblings | Raw elements |
|
|
92
|
-
| ----------------------------------- | -----------: |
|
|
93
|
-
| Mount + remove | 4.
|
|
94
|
-
| Rerender, same host props | 1.
|
|
95
|
-
| Rerender, change one host attribute | 2.
|
|
109
|
+
| Work on 10,000 siblings | Raw elements | `tasty({})` | Extra per wrapped element |
|
|
110
|
+
| ----------------------------------- | -----------: | ----------: | ------------------------: |
|
|
111
|
+
| Mount + remove | 4.2–4.3 ms | 9.2–9.7 ms | 0.50–0.54 us |
|
|
112
|
+
| Rerender, same host props | 1.2–1.3 ms | 6.3–6.5 ms | 0.51–0.52 us |
|
|
113
|
+
| Rerender, change one host attribute | 2.5–2.7 ms | 11.2–11.6 ms | 0.86–0.91 us |
|
|
96
114
|
|
|
97
115
|
The useful result is the raw/Tasty time difference divided by 10,000, not the
|
|
98
116
|
ratio between the two times. The ratio becomes large because the raw baseline
|
|
99
|
-
is tiny. In this synthetic workload, an empty wrapper adds roughly
|
|
100
|
-
participating element, or
|
|
117
|
+
is tiny. In this synthetic workload, an empty wrapper adds roughly 0.5 us per
|
|
118
|
+
participating element, or ~0.9 us when React also changes a DOM attribute.
|
|
101
119
|
|
|
102
120
|
This is the floor Tasty consumes when it has no styling job. It is not a
|
|
103
121
|
page-level score. Real trees include application components, effects, layout,
|
|
@@ -107,7 +125,7 @@ snapshot experiment with controlled garbage collection.
|
|
|
107
125
|
|
|
108
126
|
## Cold Generation and Injection
|
|
109
127
|
|
|
110
|
-
[`tasty-injection.bench.ts`](
|
|
128
|
+
[`tasty-injection.bench.ts`](https://github.com/tenphi/tasty/blob/main/src/tasty-injection.bench.ts) measures the extra
|
|
111
129
|
work when Tasty must generate and inject CSS that an otherwise equivalent page
|
|
112
130
|
already has. It does not compare different stylesheet insertion techniques.
|
|
113
131
|
|
|
@@ -142,17 +160,23 @@ ranges:
|
|
|
142
160
|
|
|
143
161
|
| Workload | CSS already present | Tasty runtime | Incremental Tasty cost |
|
|
144
162
|
| ---------------------------------------------------- | ------------------: | -------------: | ---------------------: |
|
|
145
|
-
| One new rule + immediate resolution, per transaction | 2.
|
|
146
|
-
| 1,000 new rules + one resolution | 1.
|
|
147
|
-
| 1,000-rule workload, incremental cost per rule | — | — | 7
|
|
163
|
+
| One new rule + immediate resolution, per transaction | 2.6–3.0 us | 102.8–107.0 us | 100.3–104.3 us |
|
|
164
|
+
| 1,000 new rules + one resolution | 1.69–1.79 ms | 7.42–7.67 ms | 5.73–5.94 ms |
|
|
165
|
+
| 1,000-rule workload, incremental cost per rule | — | — | 5.7–5.9 us |
|
|
148
166
|
|
|
149
167
|
Directly compared, injecting 1,000 rules before one resolution boundary cost
|
|
150
|
-
about
|
|
151
|
-
1,000 times as much. Its average incremental cost per rule was about
|
|
168
|
+
about 55–59 times as much in total as injecting one rule and resolving it—not
|
|
169
|
+
1,000 times as much. Its average incremental cost per rule was about 17–18
|
|
152
170
|
times lower. This is the same Tasty generation and injection path in both cases;
|
|
153
171
|
the group amortizes fixed transaction work and lets the browser resolve all the
|
|
154
172
|
stylesheet writes together.
|
|
155
173
|
|
|
174
|
+
The per-transaction single-rule figure is the one number here that engine work
|
|
175
|
+
does not move: it is dominated by the browser's injection-to-resolution
|
|
176
|
+
boundary, which is crossed once per rule regardless of how fast generation is.
|
|
177
|
+
The 1,000-rule column, where Tasty's own generation dominates, is where engine
|
|
178
|
+
changes show up.
|
|
179
|
+
|
|
156
180
|
The subtraction is the meaningful result. It includes Tasty's cold style
|
|
157
181
|
generation, cache and injector bookkeeping, rule insertion, and any additional
|
|
158
182
|
style invalidation exposed by that workload's resolution boundary. It does not
|
|
@@ -173,11 +197,11 @@ The benchmarks above measure mounting and whole-tree updates. A running
|
|
|
173
197
|
application spends most of its time on neither. It flips mods — hovered,
|
|
174
198
|
pressed, selected, expanded — on elements whose styles never change, one
|
|
175
199
|
element at a time, and it mounts and unmounts small styled subtrees as menus
|
|
176
|
-
and dialogs open. Both paths go through
|
|
177
|
-
|
|
200
|
+
and dialogs open. Both paths go through mod handling and the chunk cache
|
|
201
|
+
rather than the parser, so a regression in them is invisible to every
|
|
178
202
|
other benchmark here.
|
|
179
203
|
|
|
180
|
-
[`tasty-interaction.bench.tsx`](
|
|
204
|
+
[`tasty-interaction.bench.tsx`](https://github.com/tenphi/tasty/blob/main/src/tasty-interaction.bench.tsx) pairs each
|
|
181
205
|
case with a raw-DOM equivalent driven by a hand-written stylesheet that
|
|
182
206
|
produces the same computed color and background in both states. The benchmark
|
|
183
207
|
fails if either arm resolves to anything else, so an arm that quietly rendered
|
|
@@ -201,30 +225,37 @@ readable number and noise:
|
|
|
201
225
|
The browser's side of an interaction is real, but it is the browser's;
|
|
202
226
|
resolution boundaries are what the injection benchmark above measures.
|
|
203
227
|
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
The contract check also reads the injected CSS **before** any toggle and fails
|
|
229
|
+
if the hovered rule is not already there. That a style map's states all ship in
|
|
230
|
+
one chunk on first render is the premise of this case; if the hovered rule
|
|
231
|
+
arrived lazily, the first sample would be timing injection.
|
|
206
232
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
|
210
|
-
|
|
|
233
|
+
On an Apple M3 Pro with React 19.2.8 and Chromium 151, three consecutive runs:
|
|
234
|
+
|
|
235
|
+
| Workload | Raw elements | Tasty mods | Extra per unit |
|
|
236
|
+
| ---------------------------------------------------- | -----------: | ---------: | -----------------------: |
|
|
237
|
+
| 300 single-element mod toggles in a 100-element tree | 1.7–1.8 ms | 2.0–2.1 ms | 1.2–1.3 us / interaction |
|
|
238
|
+
| 20 mount + unmount cycles of a 200-element subtree | 5.2–5.4 ms | 8.3–8.6 ms | 0.77–0.81 us / element |
|
|
239
|
+
|
|
240
|
+
The absolute columns move several percent with machine load; the delta between
|
|
241
|
+
the arms is the stable quantity, so read that rather than either column.
|
|
211
242
|
|
|
212
243
|
Two things are worth reading out of this.
|
|
213
244
|
|
|
214
|
-
A mod flip on an already-mounted element costs about 1.
|
|
245
|
+
A mod flip on an already-mounted element costs about 1.2 us. The CSS for both
|
|
215
246
|
states already exists — Tasty emits every state of a style map in one chunk on
|
|
216
247
|
first render — so both arms perform the same commit, and what is left is
|
|
217
|
-
Tasty's props and mod handling. That is the same order as the ~
|
|
248
|
+
Tasty's props and mod handling. That is the same order as the ~0.5 us empty
|
|
218
249
|
wrapper measured above, which is most of where it comes from.
|
|
219
250
|
|
|
220
|
-
Subtree churn is not about styling at all. Its ~
|
|
251
|
+
Subtree churn is not about styling at all. Its ~0.8 us per element sits right
|
|
221
252
|
on the empty-wrapper mount cost, because the styles are already cached:
|
|
222
253
|
reopening a menu re-pays the React wrapper, not the style pipeline.
|
|
223
254
|
|
|
224
255
|
## Page-Load Cold Start
|
|
225
256
|
|
|
226
257
|
Every benchmark above deliberately excludes the network, module compilation and
|
|
227
|
-
the first render. [`scripts/cold-start`](
|
|
258
|
+
the first render. [`scripts/cold-start`](https://github.com/tenphi/tasty/tree/main/scripts/cold-start) measures exactly
|
|
228
259
|
those: what a visitor waits for between requesting a page and seeing styled
|
|
229
260
|
content, in a real Chromium under CDP network and CPU throttling.
|
|
230
261
|
|
|
@@ -245,39 +276,61 @@ rather than counted in animation frames — `requestAnimationFrame` fires before
|
|
|
245
276
|
paint, so a page that commits fast can reach its second frame with nothing
|
|
246
277
|
painted yet.
|
|
247
278
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
Two things about the payload decide whether this measures a deployment or a
|
|
280
|
+
straw man, so both are enforced rather than assumed:
|
|
281
|
+
|
|
282
|
+
- **Assets are served brotli-compressed**, the way a static host serves them.
|
|
283
|
+
The bundle is 51.5 KB on the wire and 180 KB decoded; putting the decoded
|
|
284
|
+
bytes on a 1.6 Mbps link would add ~700 ms and charge it to Tasty. The run
|
|
285
|
+
reads `encodedBodySize` back out of resource timing and fails if what
|
|
286
|
+
crossed the wire is not the compressed size the table reports.
|
|
287
|
+
- **The bundle is built from what the page imports** (`tasty`, `configure`,
|
|
288
|
+
`computeStyles`, `tastyDebug`), so it is tree-shaken as an application's
|
|
289
|
+
would be. Re-exporting the whole library adds ~4 KB brotli of code no page
|
|
290
|
+
here calls.
|
|
291
|
+
|
|
292
|
+
On an Apple M3 Pro with React 19.2.8 and Chromium 151, first contentful paint,
|
|
293
|
+
median of three full runs of the matrix:
|
|
294
|
+
|
|
295
|
+
| Link / CPU | baseline | runtime | prewarm | Tasty's cost | noise |
|
|
296
|
+
| --------------------- | -------: | ------: | ------: | -----------: | ----: |
|
|
297
|
+
| No throttling, 1x | 36 ms | 40 ms | 40 ms | +4 ms | 0 ms |
|
|
298
|
+
| Fast 4G, 1x | 628 ms | 672 ms | 688 ms | +44 ms | 20 ms |
|
|
299
|
+
| Slow 4G, 1x | 2044 ms | 2308 ms | 2308 ms | +264 ms | 24 ms |
|
|
300
|
+
| No throttling, 4x CPU | 112 ms | 144 ms | 144 ms | +32 ms | 16 ms |
|
|
301
|
+
| Fast 4G, 4x CPU | 656 ms | 728 ms | 736 ms | +72 ms | 4 ms |
|
|
302
|
+
| Slow 4G, 4x CPU | 2064 ms | 2360 ms | 2360 ms | +296 ms | 8 ms |
|
|
303
|
+
|
|
304
|
+
The `noise` column is not an estimate. The `baseline` page contains no Tasty at
|
|
305
|
+
all, so its three samples should be identical; the spread they actually show is
|
|
306
|
+
this cell's measurement error, and it applies to the other columns too.
|
|
307
|
+
|
|
308
|
+
Read that column before any other. Only the Slow 4G rows carry a cost an order
|
|
309
|
+
of magnitude above their own noise. The unthrottled 1x row (+4 ms) means "too
|
|
310
|
+
small to measure this way", not "4 ms". And a single run is genuinely not
|
|
311
|
+
enough here: taken alone, the first of these three runs put Fast 4G 1x at
|
|
312
|
+
+72 ms, which the median over three corrects to +44 ms.
|
|
313
|
+
|
|
314
|
+
For the same reason, `prewarm` landing above `runtime` in the Fast 4G 1x row is
|
|
315
|
+
noise, not a cost — the two modes differ only in when the engine compiles, and
|
|
316
|
+
that difference is measured in the phase table below, not in FCP.
|
|
264
317
|
|
|
265
318
|
**The cost is the bundle, not the work.** On Slow 4G the extra transfer alone
|
|
266
|
-
accounts for
|
|
267
|
-
|
|
319
|
+
accounts for 263 ms of the 264 ms FCP delta — effectively all of it. Everything
|
|
320
|
+
Tasty then *does* is small by comparison (median of three runs):
|
|
268
321
|
|
|
269
322
|
| Phase (Slow 4G, 1x) | baseline | runtime | prewarm |
|
|
270
323
|
| ----------------------- | -------: | ------: | ------: |
|
|
271
|
-
| js+css transfer |
|
|
272
|
-
| module compile (shared) |
|
|
273
|
-
| tasty top-level execute | — |
|
|
274
|
-
| `configure()` | — | 0.
|
|
275
|
-
| prewarm | — | — |
|
|
276
|
-
| render 1st component |
|
|
277
|
-
| render 49 more | 1.
|
|
324
|
+
| js+css transfer | 1423 ms | 1686 ms | 1690 ms |
|
|
325
|
+
| module compile (shared) | 3.5 ms | 3.3 ms | 3.0 ms |
|
|
326
|
+
| tasty top-level execute | — | 1.2 ms | 0.8 ms |
|
|
327
|
+
| `configure()` | — | 0.6 ms | 0.4 ms |
|
|
328
|
+
| prewarm | — | — | 3.3 ms |
|
|
329
|
+
| render 1st component | 3.0 ms | 7.3 ms | 1.9 ms |
|
|
330
|
+
| render 49 more | 1.3 ms | 5.2 ms | 3.8 ms |
|
|
278
331
|
|
|
279
332
|
Importing Tasty costs about 1 ms of top-level execution; `configure()` costs
|
|
280
|
-
half of one. The rest of the CPU delta —
|
|
333
|
+
half of one. The rest of the CPU delta — about 10 ms for 50 components — is
|
|
281
334
|
generation and injection, which is the cost the injection benchmark isolates.
|
|
282
335
|
|
|
283
336
|
One asymmetry is worth naming: the control links a render-blocking stylesheet
|
|
@@ -287,20 +340,21 @@ delivery models, not a thumb on the scale, but it means the FCP delta is not
|
|
|
287
340
|
purely "what Tasty costs to execute".
|
|
288
341
|
|
|
289
342
|
**Prewarming moves the wake-up, it does not remove it.** The first styled render
|
|
290
|
-
is ~4
|
|
343
|
+
is ~5.4 ms more expensive than the ones after it, because that is when the
|
|
291
344
|
engine's deferred payload is actually compiled. A throwaway `computeStyles()`
|
|
292
|
-
against a detached root pays it early: `render 1st` drops from
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
latency-critical path and the page
|
|
345
|
+
against a detached root pays it early: `render 1st` drops from 7.3 ms to
|
|
346
|
+
1.9 ms. The prewarm itself costs 3.3 ms, so the work is moved rather than
|
|
347
|
+
removed and FCP does not move. It is worth doing only when something else can
|
|
348
|
+
overlap it, or when the first render is on a latency-critical path and the page
|
|
349
|
+
has idle time before it.
|
|
296
350
|
|
|
297
351
|
**Retained heap.** After a forced collection, the runtime page holds about
|
|
298
|
-
|
|
352
|
+
985 KB more than the control (2,605 KB vs 1,619 KB) for 50 components — the
|
|
299
353
|
parser caches, the chunk cache, the injector's registry and the generated CSS.
|
|
300
354
|
The control is not zero either; most of its 1.6 MB is React and the DOM.
|
|
301
355
|
|
|
302
356
|
CPU throttling changes which line moves. At 4x, module compilation of the
|
|
303
|
-
larger graph becomes visible (
|
|
357
|
+
larger graph becomes visible (2.6 ms → 19 ms) where at 1x it is free: V8
|
|
304
358
|
pre-parses at import and compiles lazily, so a slower CPU pays for code the
|
|
305
359
|
faster one never fully compiled. Transfer numbers from the unthrottled cells
|
|
306
360
|
are not worth reading — with no emulated link, resource timings are scheduling
|
|
@@ -317,16 +371,17 @@ blindly. They describe different paths:
|
|
|
317
371
|
rule.
|
|
318
372
|
- A genuinely new style pays generation and injection once, then becomes
|
|
319
373
|
reusable.
|
|
320
|
-
- A mod flip on an already-styled element pays neither; it
|
|
321
|
-
|
|
374
|
+
- A mod flip on an already-styled element pays neither; it toggles a `data-*`
|
|
375
|
+
attribute and the class stays the same.
|
|
322
376
|
- Browser style resolution, layout, and paint depend on the actual document and
|
|
323
377
|
need application-level profiling.
|
|
324
378
|
|
|
325
379
|
The cold-start measurement is the one that puts the rest in proportion. On a
|
|
326
|
-
slow connection,
|
|
327
|
-
library
|
|
328
|
-
for 50 components. Bundle size is
|
|
329
|
-
|
|
380
|
+
slow connection, nearly all of Tasty's page-load cost is transferring the
|
|
381
|
+
library — 263 ms of a 264 ms delta — while the generation and injection the
|
|
382
|
+
microbenchmarks obsess over is ~10 ms for 50 components. Bundle size is
|
|
383
|
+
therefore the lever with the largest effect on first paint, and the runtime
|
|
384
|
+
levers matter for what happens after it.
|
|
330
385
|
|
|
331
386
|
The practical optimization target is therefore repeated work: keep style input
|
|
332
387
|
stable when possible, reuse generated chunks, and generate CSS at build or
|