@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.
- package/dist/astro-Dj8YpRd7.js +301 -0
- package/dist/astro-Dj8YpRd7.js.map +1 -0
- package/dist/{babel-CMYf4JZ2.d.ts → babel-Cbp2C2bj.d.ts} +2 -2
- package/dist/{collector-Ctn5s74s.d.ts → collector-DBY6liC2.d.ts} +94 -11
- package/dist/collector-NjmUiB8f.js +529 -0
- package/dist/collector-NjmUiB8f.js.map +1 -0
- package/dist/{config-B23gZrfP.js → config-Bd2vOF0Z.js} +454 -105
- package/dist/config-Bd2vOF0Z.js.map +1 -0
- package/dist/{config-CZx8_DCu.d.ts → config-CA7iAlEI.d.ts} +112 -22
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +6 -6
- package/dist/{core-B1tyn69d.js → core-ClInK6YG.js} +386 -107
- package/dist/core-ClInK6YG.js.map +1 -0
- package/dist/css-resources-Cyl_axbI.js +149 -0
- package/dist/css-resources-Cyl_axbI.js.map +1 -0
- package/dist/{css-writer-CvAxkR6S.js → css-writer-BDQXG9Jo.js} +3 -3
- package/dist/{css-writer-CvAxkR6S.js.map → css-writer-BDQXG9Jo.js.map} +1 -1
- package/dist/{format-rules-BCTWVvcS.js → format-rules-CGhgoVLR.js} +22 -32
- package/dist/format-rules-CGhgoVLR.js.map +1 -0
- package/dist/{hydrate-BCPvkJj3.js → hydrate-CqbxwVh_.js} +2 -2
- package/dist/{hydrate-BCPvkJj3.js.map → hydrate-CqbxwVh_.js.map} +1 -1
- package/dist/{index-B3o0aCYV.d.ts → index-C8RFYeFW.d.ts} +61 -12
- package/dist/{index-CsvB0EXZ.d.ts → index-FpYK0H5g.d.ts} +49 -8
- package/dist/index.d.ts +5 -5
- package/dist/index.js +43 -27
- package/dist/index.js.map +1 -1
- package/dist/{keyframes-Dxdx3KOo.js → keyframes-BjfNVhaB.js} +45 -3
- package/dist/keyframes-BjfNVhaB.js.map +1 -0
- package/dist/{merge-styles-DJTSfz_M.d.ts → merge-styles-BQv7d3fx.d.ts} +2 -2
- package/dist/{merge-styles-C-uwpoNW.js → merge-styles-BbJhjesV.js} +2 -2
- package/dist/{merge-styles-C-uwpoNW.js.map → merge-styles-BbJhjesV.js.map} +1 -1
- package/dist/precompile/index.d.ts +11 -0
- package/dist/precompile/index.js +72 -0
- package/dist/precompile/index.js.map +1 -0
- package/dist/precompile/register.d.ts +13 -0
- package/dist/precompile/register.js +2 -0
- package/dist/register-BkC-8Zuy.js +333 -0
- package/dist/register-BkC-8Zuy.js.map +1 -0
- package/dist/{resolve-recipes-BuOvSEa2.js → resolve-recipes-B2Sf36AH.js} +3 -3
- package/dist/{resolve-recipes-BuOvSEa2.js.map → resolve-recipes-B2Sf36AH.js.map} +1 -1
- package/dist/ssr/astro-client.js +1 -1
- package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
- package/dist/ssr/astro-middleware-extract-static.js +9 -0
- package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
- package/dist/ssr/astro-middleware-extract.d.ts +11 -0
- package/dist/ssr/astro-middleware-extract.js +9 -0
- package/dist/ssr/astro-middleware-extract.js.map +1 -0
- package/dist/ssr/astro-middleware-static.d.ts +3 -1
- package/dist/ssr/astro-middleware-static.js +1 -1
- package/dist/ssr/astro-middleware.d.ts +3 -1
- package/dist/ssr/astro-middleware.js +1 -1
- package/dist/ssr/astro.d.ts +45 -3
- package/dist/ssr/astro.js +1 -156
- package/dist/ssr/index.d.ts +2 -2
- package/dist/ssr/index.js +3 -3
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/next-config.d.ts +66 -0
- package/dist/ssr/next-config.js +115 -0
- package/dist/ssr/next-config.js.map +1 -0
- package/dist/ssr/next.d.ts +9 -2
- package/dist/ssr/next.js +26 -9
- package/dist/ssr/next.js.map +1 -1
- package/dist/ssr-collector-ref-COs_ioWl.js +29 -0
- package/dist/ssr-collector-ref-COs_ioWl.js.map +1 -0
- package/dist/static/index.d.ts +2 -2
- package/dist/static/index.js +1 -1
- package/dist/types-Bo5ePLZh.d.ts +111 -0
- package/dist/zero/babel.d.ts +1 -1
- package/dist/zero/babel.js +4 -4
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/index.d.ts +1 -1
- package/dist/zero/index.js +1 -1
- package/dist/zero/next.d.ts +1 -1
- package/docs/README.md +3 -1
- package/docs/debug.md +17 -6
- package/docs/injector.md +23 -3
- package/docs/precompile.md +202 -0
- package/docs/runtime-benchmarks.md +143 -72
- package/docs/ssr.md +179 -20
- package/package.json +44 -10
- package/dist/collector-DMSR6ANK.js +0 -304
- package/dist/collector-DMSR6ANK.js.map +0 -1
- package/dist/config-B23gZrfP.js.map +0 -1
- package/dist/core-B1tyn69d.js.map +0 -1
- package/dist/format-rules-BCTWVvcS.js.map +0 -1
- package/dist/keyframes-Dxdx3KOo.js.map +0 -1
- 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
|
|
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.
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
26
|
+
pnpm bench:cold-start
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
`pnpm bench` runs the core pipeline benchmarks in Node. The other
|
|
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
|
|
110
|
-
group:
|
|
111
|
+
The benchmark covers two useful workloads:
|
|
111
112
|
|
|
112
|
-
- **One new rule:** add one rule
|
|
113
|
-
computed style. Each sample performs
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
145
|
+
ranges:
|
|
141
146
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
transaction work and
|
|
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.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
210
|
-
|
|
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.
|