@tenphi/cookbook 0.28.0 → 0.30.0
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 +13 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/docs/ai-agents.md +5 -3
- package/docs/architecture.md +10 -7
- package/docs/authoring.mdx +94 -36
- package/docs/cli.md +92 -33
- package/docs/component-styles.md +286 -282
- package/docs/configuration.md +232 -153
- package/docs/content-sources.md +39 -21
- package/docs/custom-components.md +117 -169
- package/docs/customization-rules.md +23 -14
- package/docs/deployment.md +101 -53
- package/docs/es/getting-started.md +7 -5
- package/docs/es/index.md +1 -1
- package/docs/examples.md +65 -28
- package/docs/fonts-and-typography.md +32 -27
- package/docs/getting-started.md +17 -10
- package/docs/ja/getting-started.md +7 -5
- package/docs/ja/index.md +1 -1
- package/docs/migration.md +188 -65
- package/docs/plugins.md +28 -16
- package/docs/publishing.md +3 -2
- package/docs/quality-checks.md +18 -42
- package/docs/recipes.md +38 -54
- package/docs/ru/getting-started.md +7 -5
- package/docs/ru/index.md +1 -1
- package/docs/site-navigation.md +47 -47
- package/docs/theme-and-components.md +126 -59
- package/docs/troubleshooting.md +4 -3
- package/docs/upstream/manifest.json +2 -2
- package/docs/upstream/tasty/README.md +404 -381
- package/docs/upstream/tasty/docs/README.md +4 -1
- package/docs/upstream/tasty/docs/adoption.md +25 -17
- package/docs/upstream/tasty/docs/ai-agents.md +42 -34
- package/docs/upstream/tasty/docs/comparison.md +9 -8
- package/docs/upstream/tasty/docs/configuration.md +187 -69
- package/docs/upstream/tasty/docs/debug.md +3 -2
- package/docs/upstream/tasty/docs/design-system.md +117 -15
- package/docs/upstream/tasty/docs/dsl.md +211 -117
- package/docs/upstream/tasty/docs/getting-started.md +81 -24
- package/docs/upstream/tasty/docs/injector.md +41 -40
- package/docs/upstream/tasty/docs/methodology.md +46 -21
- package/docs/upstream/tasty/docs/migration-v3.md +3 -3
- package/docs/upstream/tasty/docs/pipeline.md +111 -116
- package/docs/upstream/tasty/docs/plugins.md +21 -41
- package/docs/upstream/tasty/docs/react-api.md +73 -10
- package/docs/upstream/tasty/docs/runtime-benchmarks.md +46 -46
- package/docs/upstream/tasty/docs/ssr.md +92 -66
- package/docs/upstream/tasty/docs/styles.md +37 -17
- package/docs/upstream/tasty/docs/tasty-static.md +175 -57
- package/docs/zh/getting-started.md +7 -5
- package/docs/zh/index.md +1 -1
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use `tasty()` to turn state-aware style definitions into normal React components, extend existing Tasty components, and expose typed styling APIs to consumers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Every Tasty style function — `tasty()` components, `computeStyles()`, `useStyles()`, `useGlobalStyles()`, `useRawCSS()`, `useKeyframes()`, `useProperty()`, `useFontFace()`, `useCounterStyle()`, and `useFunction()` — works in React Server Components with no `'use client'` directive. All of them except `useStyles()` are hook-free. `useStyles()` reads the `TastyRegistry` collector from React context on the client, so it is a real hook there; outside React, call `computeStyles()` instead. For state maps, tokens, units, and extension semantics, see [Style DSL](dsl.md). For global configuration, see [Configuration](configuration.md). For the broader docs map, see the [Docs Hub](README.md).
|
|
6
6
|
|
|
7
7
|
> **Note:** This file was previously named `runtime.md`. All functionality documented here works in both server and client contexts — "runtime" referred to style computation during React rendering, not to client-side JavaScript.
|
|
8
8
|
|
|
@@ -44,6 +44,30 @@ const PrimaryButton = tasty(Button, {
|
|
|
44
44
|
|
|
45
45
|
Style maps merge intelligently — see [Style DSL — Extending vs. Replacing State Maps](dsl.md#extending-vs-replacing-state-maps) for extend mode, replace mode, `@inherit`, `null`, and `false` tombstones.
|
|
46
46
|
|
|
47
|
+
The extension form takes the same component options as `tasty({ … })`:
|
|
48
|
+
|
|
49
|
+
```jsx
|
|
50
|
+
const ActionCard = tasty(Card, {
|
|
51
|
+
styles: { fill: { '': '#surface', isRaised: '#surface-raised' } },
|
|
52
|
+
styleProps: ['gap'] as const, // gap="2x" becomes a style
|
|
53
|
+
modProps: ['isRaised'] as const, // isRaised becomes a mod (data-is-raised)
|
|
54
|
+
tokenProps: ['accentColor'] as const, // accentColor sets the #accent token
|
|
55
|
+
variants: {
|
|
56
|
+
plain: { border: 0 },
|
|
57
|
+
framed: { border: '2bw #accent' },
|
|
58
|
+
},
|
|
59
|
+
variant: 'framed', // applied when no variant prop is passed
|
|
60
|
+
elements: { Footer: 'footer' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
<ActionCard gap="2x" isRaised accentColor="#purple">
|
|
64
|
+
<ActionCard.Title>Kept from Card</ActionCard.Title>
|
|
65
|
+
<ActionCard.Footer>Added here</ActionCard.Footer>
|
|
66
|
+
</ActionCard>;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The extension turns its own `styleProps`, `modProps`, and `tokenProps` into the `styles`, `mods`, and `tokens` it passes to the base, which renders them as usual: mods as `data-*` attributes, tokens as inline custom properties. Its `variants` merge over its `styles`, and the `variant` prop is still forwarded, so it selects the base's own variants too and accepts either set's names. Both sets share that one prop, so a name only one of them defines leaves the other at its fallback: if `Card` had an `accent` variant, `variant="accent"` would apply it together with `framed`, and `variant="plain"` would leave `Card` on its default. For two independent choices, give one of them its own prop, such as a `modProps` value list. `elements` adds sub-components, and the base's are kept: `ActionCard.Title` is `Card.Title`. The returned component is typed with all of these props and sub-components.
|
|
70
|
+
|
|
47
71
|
`tasty(Component, ...)` always wraps `Component` and forwards every prop to it, so `Component` must be Tasty-aware (i.e. it accepts `styles`, `mods`, `qa`, etc. and renders them through its own Tasty pipeline). To apply styles to a third-party component or a string DOM tag via `className`, use the options-only form with `as`:
|
|
48
72
|
|
|
49
73
|
```jsx
|
|
@@ -68,7 +92,7 @@ const Span = tasty({
|
|
|
68
92
|
</Link>;
|
|
69
93
|
```
|
|
70
94
|
|
|
71
|
-
The wrapped component only needs to forward `className` (and ideally `style`/`ref`). Tasty
|
|
95
|
+
The wrapped component only needs to forward `className` (and ideally `style`/`ref`). Tasty's own props (`styles`, `mods`, `tokens`, `qa`, `qaVal`, `theme`, `element`, the `is*` props) and the props you declared with `styleProps`, `modProps`, or `tokenProps` are consumed by Tasty; mods, `qa`, `qaVal`, `theme`, and `element` reach the DOM only as `data-*` attributes.
|
|
72
96
|
|
|
73
97
|
Props forwarded to the `as` component keep their declared types and requiredness. For example, a component with `position: 'top-right' | 'bottom-left'` accepts those values through `tasty({ as: Component })`; CSS positioning can still be set through `styles.position`. A prop explicitly listed in `styleProps` is consumed as a style value instead. The always-available base style props below are also consumed by Tasty, so keep component-specific prop names separate from those reserved names.
|
|
74
98
|
|
|
@@ -112,12 +136,12 @@ A small set of style properties — `display`, `font`, `preset`, `hide`, `whiteS
|
|
|
112
136
|
|
|
113
137
|
## `Element`
|
|
114
138
|
|
|
115
|
-
`Element` is the unstyled base component exported from the main entry — equivalent to `tasty({})` (a `div` with no default styles). It accepts
|
|
139
|
+
`Element` is the unstyled base component exported from the main entry — equivalent to `tasty({})` (a `div` with no default styles). It accepts the props every Tasty component has (`as`, `styles`, `mods`, `tokens`, `qa`, `theme`, the `is*` props) plus the [always-available style props](#always-available-style-props), and is useful as a generic box or as a building block for layout primitives. Other style names, such as `padding`, are not props on it: create a component with `styleProps` for those, or pass them through `styles`.
|
|
116
140
|
|
|
117
141
|
```jsx
|
|
118
142
|
import { Element } from '@tenphi/tasty';
|
|
119
143
|
|
|
120
|
-
<Element as="section"
|
|
144
|
+
<Element as="section" display="grid" preset="t2" styles={{ padding: '4x' }}>
|
|
121
145
|
Content
|
|
122
146
|
</Element>;
|
|
123
147
|
```
|
|
@@ -144,6 +168,7 @@ List modifier key names. Types default to `ModValue` (`boolean | string | number
|
|
|
144
168
|
|
|
145
169
|
```jsx
|
|
146
170
|
const Button = tasty({
|
|
171
|
+
as: 'button',
|
|
147
172
|
modProps: ['isLoading', 'isSelected'] as const,
|
|
148
173
|
styles: {
|
|
149
174
|
fill: { '': '#surface', isLoading: '#surface.5' },
|
|
@@ -161,6 +186,7 @@ Map modifier names to type descriptors for precise TypeScript types:
|
|
|
161
186
|
|
|
162
187
|
```tsx
|
|
163
188
|
const Button = tasty({
|
|
189
|
+
as: 'button',
|
|
164
190
|
modProps: {
|
|
165
191
|
isLoading: Boolean, // isLoading?: boolean
|
|
166
192
|
isSelected: Boolean, // isSelected?: boolean
|
|
@@ -297,6 +323,24 @@ const Button = tasty({
|
|
|
297
323
|
<Button variant="danger">Delete</Button>;
|
|
298
324
|
```
|
|
299
325
|
|
|
326
|
+
With `variant` omitted, or set to a key `variants` does not define, the `default` variant applies; without a `default` key only the base `styles` apply. `variant` is typed from the variant keys.
|
|
327
|
+
|
|
328
|
+
To fall back to another variant instead, name it in the `variant` option:
|
|
329
|
+
|
|
330
|
+
```jsx
|
|
331
|
+
const Button = tasty({
|
|
332
|
+
styles: { padding: '2x 4x' },
|
|
333
|
+
variants: {
|
|
334
|
+
primary: { fill: '#blue', color: '#white' },
|
|
335
|
+
danger: { fill: '#red', color: '#white' },
|
|
336
|
+
},
|
|
337
|
+
variant: 'primary',
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
<Button>Save</Button>; // primary
|
|
341
|
+
<Button variant="danger">Delete</Button>;
|
|
342
|
+
```
|
|
343
|
+
|
|
300
344
|
### Extending Variants with Base State Maps
|
|
301
345
|
|
|
302
346
|
When base `styles` contain an extend-mode state map (an object **without** a `''` key), it is applied **after** the variant merge. This lets you add or override states across all variants without repeating yourself:
|
|
@@ -362,7 +406,7 @@ elements: {
|
|
|
362
406
|
}
|
|
363
407
|
```
|
|
364
408
|
|
|
365
|
-
The sub-components produced by `elements` support `mods`, `tokens`, `isDisabled`, `isHidden`, and `isChecked` props — the same modifier interface as the root component.
|
|
409
|
+
The sub-components produced by `elements` support `mods`, `tokens`, `theme`, `isDisabled`, `isHidden`, and `isChecked` props — the same modifier interface as the root component. As on the root, `theme` renders as `data-theme`, and an explicit `data-theme` wins over it.
|
|
366
410
|
|
|
367
411
|
If you don't need sub-components (e.g., the inner elements are already rendered by a third-party library), you can still style them by key alone — just omit `elements` and apply `data-element` manually:
|
|
368
412
|
|
|
@@ -401,19 +445,19 @@ const { className } = computeStyles({
|
|
|
401
445
|
});
|
|
402
446
|
```
|
|
403
447
|
|
|
404
|
-
On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server, CSS
|
|
448
|
+
On the client, CSS is injected synchronously into the DOM (idempotent via the injector cache). On the server with an SSR collector (from `runWithCollector()` or `TastyRegistry`), the CSS goes to the collector. With neither a collector nor a `document` — a React Server Component — nothing is injected: the CSS comes back as `css`, and you render it in a `style` tag yourself, as `tasty()` components do. This is the same function that `tasty()` components use internally.
|
|
405
449
|
|
|
406
450
|
---
|
|
407
451
|
|
|
408
452
|
## Style Functions
|
|
409
453
|
|
|
410
|
-
All style functions below
|
|
454
|
+
All style functions below work in client components, SSR with a `ServerStyleCollector`, and React Server Components. All except `useStyles()` are plain functions, not React hooks; they keep the `use` prefix for backward compatibility.
|
|
411
455
|
|
|
412
456
|
In server-only contexts, components that use only Tasty style functions ship no Tasty styling runtime. Astro without `client:*` directives produces no client JavaScript; server-only Next.js RSC follows the same Tasty architecture, while final output depends on the application deployment. Tasty never forces the `'use client'` boundary — that decision belongs to your component when it needs React interactivity (state, effects, event handlers).
|
|
413
457
|
|
|
414
458
|
### useStyles
|
|
415
459
|
|
|
416
|
-
Generate a className from a style object.
|
|
460
|
+
Generate a className from a style object. A thin wrapper around `computeStyles()` that also picks up the `TastyRegistry` collector through React context. That makes it a hook on the client: call it inside a component, and use `computeStyles()` outside React. It also works in a Server Component, where it skips the context lookup; like the other style functions there, it cannot emit CSS itself, so the next `tasty()` element in the tree emits it.
|
|
417
461
|
|
|
418
462
|
```tsx
|
|
419
463
|
import { useStyles } from '@tenphi/tasty';
|
|
@@ -447,6 +491,17 @@ function ThemeStyles() {
|
|
|
447
491
|
}
|
|
448
492
|
```
|
|
449
493
|
|
|
494
|
+
The selector can be a list (`'.uni-checkbox, uni-checkbox'`) — every item gets
|
|
495
|
+
the states and sub-elements, with no `:is()` wrapping needed. Root states
|
|
496
|
+
(`@root(...)`) on a selector that is the root itself (`:root`, `html`) apply to
|
|
497
|
+
that element. A selector that ends in a pseudo-element takes its states on the
|
|
498
|
+
pseudo-element (`'.card::before'` with `:hover` gives
|
|
499
|
+
`.card::before:where(:hover)`) and `@parent()` on the element before it.
|
|
500
|
+
Attribute and modifier states, which a pseudo-element can't have, are ignored
|
|
501
|
+
with a warning. Local `@keyframes` are injected with the rules,
|
|
502
|
+
under the same resolved names `tasty()` components use, and `@starting` states
|
|
503
|
+
emit `@starting-style`.
|
|
504
|
+
|
|
450
505
|
A slot — the `id`, or the selector when no `id` is given — holds exactly one
|
|
451
506
|
injection **per `root`**. So:
|
|
452
507
|
|
|
@@ -475,6 +530,8 @@ function GlobalReset() {
|
|
|
475
530
|
An `id` slot holds one injection per `root`. Without an `id` the CSS is deduped
|
|
476
531
|
by content and permanent — there is nothing to replace it with later.
|
|
477
532
|
|
|
533
|
+
It also accepts a factory and deps — `useRawCSS(() => css, [deps], { id })` — and skips the factory while the deps for that `id` are unchanged.
|
|
534
|
+
|
|
478
535
|
### useKeyframes
|
|
479
536
|
|
|
480
537
|
Inject `@keyframes` rules and return the generated animation name:
|
|
@@ -584,7 +641,11 @@ useFontFace('Brand Sans', [
|
|
|
584
641
|
Signature:
|
|
585
642
|
|
|
586
643
|
```ts
|
|
587
|
-
function useFontFace(
|
|
644
|
+
function useFontFace(
|
|
645
|
+
family: string,
|
|
646
|
+
input: FontFaceInput,
|
|
647
|
+
options?: { root?: Document | ShadowRoot },
|
|
648
|
+
): void;
|
|
588
649
|
```
|
|
589
650
|
|
|
590
651
|
### useCounterStyle
|
|
@@ -666,9 +727,10 @@ See the [Functions section of the DSL reference](dsl.md#functions-function) for
|
|
|
666
727
|
|
|
667
728
|
- Styles are not updating: make sure `configure()` runs before first render, and verify the generated class name or global rule with [Debug Utilities](debug.md).
|
|
668
729
|
- SSR output looks wrong: check the [SSR guide](ssr.md) for collector setup. All style functions discover the SSR collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`.
|
|
730
|
+
- "Server rendering without a style collector" in development: the server render has no request scope, so each `tasty()` element inlines its own CSS and configured globals are not emitted. Add `tastyIntegration()` (Astro), `TastyRegistry` (Next.js), or `runWithCollector()` — see the [SSR guide](ssr.md).
|
|
669
731
|
- Animation/custom property issues: prefer `useKeyframes()` and `useProperty()` over raw CSS when you want Tasty to manage injection and SSR collection for you.
|
|
670
732
|
- For dynamic styles that change over the component lifecycle, use the `id` option in `useGlobalStyles()` and `useRawCSS()` to enable update tracking.
|
|
671
|
-
- RSC inline mode: CSS accumulated by standalone style functions (`useGlobalStyles`, `useRawCSS`, etc.) is flushed into inline `<style>` tags by the next `tasty()` component in the render tree. If your page uses only standalone style functions without any `tasty()` component, the CSS will not be emitted. Ensure at least one `tasty()` component is present in each RSC render tree.
|
|
733
|
+
- RSC inline mode: CSS accumulated by standalone style functions (`useStyles`, `useGlobalStyles`, `useRawCSS`, etc.) is flushed into inline `<style>` tags by the next `tasty()` component in the render tree. If your page uses only standalone style functions without any `tasty()` component, the CSS will not be emitted. Ensure at least one `tasty()` component is present in each RSC render tree.
|
|
672
734
|
|
|
673
735
|
---
|
|
674
736
|
|
|
@@ -677,6 +739,7 @@ See the [Functions section of the DSL reference](dsl.md#functions-function) for
|
|
|
677
739
|
- **[Style DSL](dsl.md)** — State maps, tokens, units, extending semantics, keyframes, @property
|
|
678
740
|
- **[Methodology](methodology.md)** — Recommended patterns: root + sub-elements, styleProps, tokens, wrapping
|
|
679
741
|
- **[Configuration](configuration.md)** — Tokens, recipes, custom units, style handlers, TypeScript extensions
|
|
742
|
+
- **[Configuration → TypeScript](configuration.md#typescript)** — How props are typed and how to register your names
|
|
680
743
|
- **[Style Properties](styles.md)** — Complete reference for all enhanced style properties
|
|
681
744
|
- **[Build-Time Extraction (`tastyStatic`)](tasty-static.md)** — Static styling with the Babel plugin
|
|
682
745
|
- **[Server-Side Rendering](ssr.md)** — SSR setup for Next.js, Astro, and generic frameworks
|
|
@@ -63,15 +63,15 @@ Single-call throughput, three consecutive runs on an Apple M3 Pro with Node
|
|
|
63
63
|
| `renderStyles` — 5 flat properties (cold) | 143,000–158,000 | 6.3–7.0 us |
|
|
64
64
|
| `renderStyles` — state map with media/hover/modifier (cold) | 31,000–36,000 | 28–32 us |
|
|
65
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 |
|
|
67
|
-
| `parseStateKey` — simple key like `:hover` (cold) | 1,910,000–1,980,000 |
|
|
68
|
-
| `parseStateKey` — value modifier key (cold) | 1,120,000–1,160,000 |
|
|
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
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 |
|
|
71
|
-
| `parseStyle` — value tokens like `2x 4x` (cold) | 575,000–605,000 |
|
|
72
|
-
| `parseStyle` — color tokens (cold) | 1,160,000–1,240,000 |
|
|
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
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 |
|
|
74
|
+
| `parseStyle` — any value (cached) | 29,500,000–30,750,000 | ~0.03 us |
|
|
75
75
|
|
|
76
76
|
“Cold” cases use unique inputs to bypass the relevant caches. Cached cases
|
|
77
77
|
reuse one input and measure the LRU hot path. Run-to-run spread on an idle
|
|
@@ -80,9 +80,9 @@ cold cases. These benchmarks do not include React, DOM work, stylesheet
|
|
|
80
80
|
injection, style resolution, layout, or paint.
|
|
81
81
|
|
|
82
82
|
The benchmark sources are colocated with the code they exercise:
|
|
83
|
-
[`pipeline.bench.ts`](https://github.com/tenphi/tasty/blob/
|
|
84
|
-
[`parseStateKey.bench.ts`](https://github.com/tenphi/tasty/blob/
|
|
85
|
-
parser benchmark files under [`src/parser`](https://github.com/tenphi/tasty/
|
|
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).
|
|
86
86
|
|
|
87
87
|
## Empty Wrapper Overhead
|
|
88
88
|
|
|
@@ -91,7 +91,7 @@ Skipping the style pipeline does not make a `tasty()` component free. Even
|
|
|
91
91
|
tracks another fiber, and Tasty still processes and forwards the element's
|
|
92
92
|
props.
|
|
93
93
|
|
|
94
|
-
[`tasty-overhead.bench.tsx`](https://github.com/tenphi/tasty/blob/
|
|
94
|
+
[`tasty-overhead.bench.tsx`](https://github.com/tenphi/tasty/blob/main/src/tasty-overhead.bench.tsx) compares 10,000
|
|
95
95
|
raw `<div className>` siblings with 10,000 instances of one module-scoped
|
|
96
96
|
`tasty({})` component. Both receive the same props, and the benchmark fails if
|
|
97
97
|
they do not produce equivalent DOM.
|
|
@@ -106,16 +106,16 @@ sample. This does not estimate React's concurrent scheduling latency.
|
|
|
106
106
|
On an Apple M3 Pro with React 19.2.8 and Chromium 151, three consecutive runs
|
|
107
107
|
produced these ranges:
|
|
108
108
|
|
|
109
|
-
| Work on 10,000 siblings | Raw elements |
|
|
110
|
-
| ----------------------------------- | -----------: |
|
|
111
|
-
| Mount + remove | 4.2–4.3 ms |
|
|
112
|
-
| Rerender, same host props | 1.2–1.3 ms |
|
|
113
|
-
| Rerender, change one host attribute | 2.5–2.7 ms | 11.2–11.6 ms |
|
|
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 |
|
|
114
114
|
|
|
115
115
|
The useful result is the raw/Tasty time difference divided by 10,000, not the
|
|
116
116
|
ratio between the two times. The ratio becomes large because the raw baseline
|
|
117
117
|
is tiny. In this synthetic workload, an empty wrapper adds roughly 0.5 us per
|
|
118
|
-
participating element, or
|
|
118
|
+
participating element, or ~0.9 us when React also changes a DOM attribute.
|
|
119
119
|
|
|
120
120
|
This is the floor Tasty consumes when it has no styling job. It is not a
|
|
121
121
|
page-level score. Real trees include application components, effects, layout,
|
|
@@ -125,18 +125,18 @@ snapshot experiment with controlled garbage collection.
|
|
|
125
125
|
|
|
126
126
|
## Cold Generation and Injection
|
|
127
127
|
|
|
128
|
-
[`tasty-injection.bench.ts`](https://github.com/tenphi/tasty/blob/
|
|
128
|
+
[`tasty-injection.bench.ts`](https://github.com/tenphi/tasty/blob/main/src/tasty-injection.bench.ts) measures the extra
|
|
129
129
|
work when Tasty must generate and inject CSS that an otherwise equivalent page
|
|
130
130
|
already has. It does not compare different stylesheet insertion techniques.
|
|
131
131
|
|
|
132
132
|
The benchmark covers two useful workloads:
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
- **One new rule:** add one rule to an existing stylesheet, append its one
|
|
135
135
|
element, and immediately read its computed style. Each timed sample performs
|
|
136
136
|
50 independent one-rule transactions and reports their total; dividing the
|
|
137
137
|
raw/Tasty difference by 50 produces a stable per-rule result despite
|
|
138
138
|
Chromium's 0.1 ms timer resolution.
|
|
139
|
-
|
|
139
|
+
- **1,000 new rules together:** generate and insert all 1,000 rules into one
|
|
140
140
|
stylesheet, append all 1,000 elements, then read every computed color without
|
|
141
141
|
another write in between. This gives the browser one style-resolution
|
|
142
142
|
boundary for the group.
|
|
@@ -197,11 +197,11 @@ The benchmarks above measure mounting and whole-tree updates. A running
|
|
|
197
197
|
application spends most of its time on neither. It flips mods — hovered,
|
|
198
198
|
pressed, selected, expanded — on elements whose styles never change, one
|
|
199
199
|
element at a time, and it mounts and unmounts small styled subtrees as menus
|
|
200
|
-
and dialogs open. Both paths go through
|
|
201
|
-
|
|
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
|
|
202
202
|
other benchmark here.
|
|
203
203
|
|
|
204
|
-
[`tasty-interaction.bench.tsx`](https://github.com/tenphi/tasty/blob/
|
|
204
|
+
[`tasty-interaction.bench.tsx`](https://github.com/tenphi/tasty/blob/main/src/tasty-interaction.bench.tsx) pairs each
|
|
205
205
|
case with a raw-DOM equivalent driven by a hand-written stylesheet that
|
|
206
206
|
produces the same computed color and background in both states. The benchmark
|
|
207
207
|
fails if either arm resolves to anything else, so an arm that quietly rendered
|
|
@@ -216,12 +216,12 @@ performs 20 open/close cycles. Divide the raw/Tasty difference by those counts.
|
|
|
216
216
|
Two things had to be sized deliberately, and both are the difference between a
|
|
217
217
|
readable number and noise:
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
- **The tree is small (100 elements), not large.** React locates a leaf's
|
|
220
220
|
pending update by walking the sibling list, so in a 1,000-element tree a
|
|
221
|
-
single-element update costs
|
|
221
|
+
single-element update costs ~68 us of traversal — identical in both arms and
|
|
222
222
|
an order of magnitude above anything the styling layer contributes.
|
|
223
|
-
|
|
224
|
-
between flips costs
|
|
223
|
+
- **The sample resolves style once, not once per flip.** Forcing a recalc
|
|
224
|
+
between flips costs ~70 us in both arms, which buries the delta the same way.
|
|
225
225
|
The browser's side of an interaction is real, but it is the browser's;
|
|
226
226
|
resolution boundaries are what the injection benchmark above measures.
|
|
227
227
|
|
|
@@ -235,7 +235,7 @@ On an Apple M3 Pro with React 19.2.8 and Chromium 151, three consecutive runs:
|
|
|
235
235
|
| Workload | Raw elements | Tasty mods | Extra per unit |
|
|
236
236
|
| ---------------------------------------------------- | -----------: | ---------: | -----------------------: |
|
|
237
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
|
|
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
239
|
|
|
240
240
|
The absolute columns move several percent with machine load; the delta between
|
|
241
241
|
the arms is the stable quantity, so read that rather than either column.
|
|
@@ -245,29 +245,29 @@ Two things are worth reading out of this.
|
|
|
245
245
|
A mod flip on an already-mounted element costs about 1.2 us. The CSS for both
|
|
246
246
|
states already exists — Tasty emits every state of a style map in one chunk on
|
|
247
247
|
first render — so both arms perform the same commit, and what is left is
|
|
248
|
-
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
|
|
249
249
|
wrapper measured above, which is most of where it comes from.
|
|
250
250
|
|
|
251
|
-
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
|
|
252
252
|
on the empty-wrapper mount cost, because the styles are already cached:
|
|
253
253
|
reopening a menu re-pays the React wrapper, not the style pipeline.
|
|
254
254
|
|
|
255
255
|
## Page-Load Cold Start
|
|
256
256
|
|
|
257
257
|
Every benchmark above deliberately excludes the network, module compilation and
|
|
258
|
-
the first render. [`scripts/cold-start`](https://github.com/tenphi/tasty/
|
|
258
|
+
the first render. [`scripts/cold-start`](https://github.com/tenphi/tasty/tree/main/scripts/cold-start) measures exactly
|
|
259
259
|
those: what a visitor waits for between requesting a page and seeing styled
|
|
260
260
|
content, in a real Chromium under CDP network and CPU throttling.
|
|
261
261
|
|
|
262
262
|
Three pages render the same 50 styled components and are verified, before any
|
|
263
263
|
timing, to produce the same 50 elements at the same computed color:
|
|
264
264
|
|
|
265
|
-
|
|
265
|
+
- **baseline** — the components server-rendered: identical markup, identical
|
|
266
266
|
class names, a linked stylesheet, and no Tasty on the page. Every other
|
|
267
267
|
column is a delta against this one.
|
|
268
|
-
|
|
268
|
+
- **runtime** — Tasty generates the CSS in the browser, as a client-rendered
|
|
269
269
|
application does. The run asserts it really did (69 rules generated).
|
|
270
|
-
|
|
270
|
+
- **prewarm** — the same, after one throwaway `computeStyles()` against a
|
|
271
271
|
detached root before the first component renders.
|
|
272
272
|
|
|
273
273
|
Each cell is the median of 5 uncached loads in a fresh browser context. The run
|
|
@@ -279,14 +279,14 @@ painted yet.
|
|
|
279
279
|
Two things about the payload decide whether this measures a deployment or a
|
|
280
280
|
straw man, so both are enforced rather than assumed:
|
|
281
281
|
|
|
282
|
-
|
|
282
|
+
- **Assets are served brotli-compressed**, the way a static host serves them.
|
|
283
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
|
|
284
|
+
bytes on a 1.6 Mbps link would add ~700 ms and charge it to Tasty. The run
|
|
285
285
|
reads `encodedBodySize` back out of resource timing and fails if what
|
|
286
286
|
crossed the wire is not the compressed size the table reports.
|
|
287
|
-
|
|
287
|
+
- **The bundle is built from what the page imports** (`tasty`, `configure`,
|
|
288
288
|
`computeStyles`, `tastyDebug`), so it is tree-shaken as an application's
|
|
289
|
-
would be. Re-exporting the whole library adds
|
|
289
|
+
would be. Re-exporting the whole library adds ~4 KB brotli of code no page
|
|
290
290
|
here calls.
|
|
291
291
|
|
|
292
292
|
On an Apple M3 Pro with React 19.2.8 and Chromium 151, first contentful paint,
|
|
@@ -340,7 +340,7 @@ delivery models, not a thumb on the scale, but it means the FCP delta is not
|
|
|
340
340
|
purely "what Tasty costs to execute".
|
|
341
341
|
|
|
342
342
|
**Prewarming moves the wake-up, it does not remove it.** The first styled render
|
|
343
|
-
is
|
|
343
|
+
is ~5.4 ms more expensive than the ones after it, because that is when the
|
|
344
344
|
engine's deferred payload is actually compiled. A throwaway `computeStyles()`
|
|
345
345
|
against a detached root pays it early: `render 1st` drops from 7.3 ms to
|
|
346
346
|
1.9 ms. The prewarm itself costs 3.3 ms, so the work is moved rather than
|
|
@@ -365,21 +365,21 @@ jitter.
|
|
|
365
365
|
Do not add the microbenchmark numbers together to estimate an application
|
|
366
366
|
blindly. They describe different paths:
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
- A stable `tasty()` factory can skip the style pipeline on later renders, but
|
|
369
369
|
its React wrapper still participates in reconciliation.
|
|
370
|
-
|
|
370
|
+
- A cached style avoids cold parsing and generation and does not insert a new
|
|
371
371
|
rule.
|
|
372
|
-
|
|
372
|
+
- A genuinely new style pays generation and injection once, then becomes
|
|
373
373
|
reusable.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
- A mod flip on an already-styled element pays neither; it toggles a `data-*`
|
|
375
|
+
attribute and the class stays the same.
|
|
376
|
+
- Browser style resolution, layout, and paint depend on the actual document and
|
|
377
377
|
need application-level profiling.
|
|
378
378
|
|
|
379
379
|
The cold-start measurement is the one that puts the rest in proportion. On a
|
|
380
380
|
slow connection, nearly all of Tasty's page-load cost is transferring the
|
|
381
381
|
library — 263 ms of a 264 ms delta — while the generation and injection the
|
|
382
|
-
microbenchmarks obsess over is
|
|
382
|
+
microbenchmarks obsess over is ~10 ms for 50 components. Bundle size is
|
|
383
383
|
therefore the lever with the largest effect on first paint, and the runtime
|
|
384
384
|
levers matter for what happens after it.
|
|
385
385
|
|