claude-toolkit 0.1.27 → 0.10.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.
@@ -107,7 +107,7 @@ await page.route("**/analytics/**", (route) => route.abort());
107
107
 
108
108
  Mock ~80% of API calls for speed; keep ~20% hitting real endpoints. Always call `route.continue()`, `route.fulfill()`, or `route.abort()`. Use `page.unrouteAll()` in cleanup.
109
109
 
110
- ## WebSocket Mocking (v1.53+)
110
+ ## WebSocket Mocking (v1.48+)
111
111
 
112
112
  ```typescript
113
113
  await page.routeWebSocket("wss://example.com/ws", (ws) => {
@@ -127,7 +127,7 @@ test("WCAG 2.1 AA", async ({ page }) => {
127
127
  });
128
128
  ```
129
129
 
130
- Aria snapshots (v1.52+): `await expect(nav).toMatchAriaSnapshot(...)`.
130
+ Aria snapshots (v1.49+): `await expect(nav).toMatchAriaSnapshot(...)`.
131
131
 
132
132
  ## Parallelization
133
133
 
@@ -137,6 +137,65 @@ Aria snapshots (v1.52+): `await expect(nav).toMatchAriaSnapshot(...)`.
137
137
 
138
138
  Each worker gets its own `BrowserContext` (isolated cookies/storage). Use `mode: "serial"` sparingly.
139
139
 
140
+ ## Test Speed
141
+
142
+ > _Verified against Playwright 1.5x (2026-06)._
143
+
144
+ E2E wall-clock is dominated by browser launches and UI setup. Three levers: maximize parallelism, seed state over HTTP, fan out across CI.
145
+
146
+ ```typescript
147
+ // playwright.config.ts
148
+ export default defineConfig({
149
+ fullyParallel: true, // parallelize within files, not just across them
150
+ workers: process.env.CI ? "50%" : undefined, // track the runner's real vCPUs; undefined = 50% cores locally
151
+ webServer: {
152
+ command: "npm run preview", // prebuilt/preview server, not a dev build
153
+ url: "http://localhost:4173",
154
+ reuseExistingServer: !process.env.CI, // reuse on local reruns; CI always boots fresh
155
+ timeout: 120_000,
156
+ },
157
+ });
158
+ ```
159
+
160
+ > Don't hardcode `workers` to a number larger than the runner. GitHub standard runners are 2 vCPU (private) / 4 (public); oversubscribing thrashes. `"50%"` (or a count you've matched to the runner) is portable.
161
+
162
+ **Seed state over HTTP, not the UI.** Driving the browser to create fixtures/auth is the costliest per-test work. Do it once in the `setup` project via the `request` fixture (no browser, no UI clicks), then reuse `storageState` — same project-dependency pattern this stack already uses for UI login:
163
+
164
+ ```typescript
165
+ // e2e/auth.setup.ts (consumed via dependencies: ["setup"] + storageState in config)
166
+ import { test as setup } from "@playwright/test";
167
+
168
+ setup("authenticate via API", async ({ request }) => {
169
+ await request.post("/api/login", {
170
+ data: { email: process.env.E2E_EMAIL, password: process.env.E2E_PASSWORD },
171
+ });
172
+ await request.storageState({ path: "e2e/.auth/user.json" });
173
+ });
174
+ ```
175
+
176
+ The `request` fixture is a full HTTP client (it wraps `apiRequest.newContext()` under the hood) and carries cookies set by the response — no manual context to dispose. Keep credentials in env/secrets, never literals, and keep `e2e/.auth/` gitignored (it holds live session tokens).
177
+
178
+ **Shard across CI runners** (blob reporter + `merge-reports`, v1.37+):
179
+
180
+ ```yaml
181
+ strategy:
182
+ matrix: { shard: [1, 2, 3, 4] }
183
+ steps:
184
+ - run: npx playwright test --shard=${{ matrix.shard }}/4
185
+ # reporter: process.env.CI ? "blob" : "html" in config
186
+ # upload blob-report/, then a dependent job:
187
+ - run: npx playwright merge-reports --reporter=html ./all-blob-reports
188
+ ```
189
+
190
+ Shards stay balanced only when `fullyParallel: true` lets Playwright split at the test level; otherwise file-level granularity leaves wall-clock gated by the slowest shard.
191
+
192
+ | Lever | Effect |
193
+ |---|---|
194
+ | `fullyParallel: true` + `workers` | Near-linear speedup up to vCPU count |
195
+ | `request` fixture seeding | Cuts per-test setup from seconds to ms |
196
+ | `--shard=i/n` across runners | ~total/n with `fullyParallel`; uneven (gated by slowest shard) without it |
197
+ | `trace: "on-first-retry"` | No trace cost on the passing path |
198
+
140
199
  ## CI/CD
141
200
 
142
201
  - Use official Playwright Docker image (`mcr.microsoft.com/playwright:v1.58.0-noble`)
@@ -166,3 +225,12 @@ Each worker gets its own `BrowserContext` (isolated cookies/storage). Use `mode:
166
225
  6. **Using Playwright for unit tests** -- Use Vitest for pure logic.
167
226
  7. **Committing raw codegen output** -- Always refactor into Page Objects.
168
227
  8. **Not cleaning up route handlers** -- Use `page.unrouteAll()`.
228
+ 9. **Logging in / seeding data through the UI per test** -- Use the `request` fixture (or `context.request`) to seed state over HTTP, save it to `storageState`, reuse via project `dependencies`.
229
+ 10. **Hardcoding `workers` above the runner's vCPUs on CI** -- Defaults to 50% of *logical* cores; throttled/oversubscribed runners thrash. Use `"50%"` or pin to the runner's real vCPU count.
230
+ 11. **`--shard` without the blob reporter + `merge-reports`** -- Produces fragmented/duplicate reports instead of one merged HTML.
231
+ 12. **Booting a dev server (HMR/rebuild) as the `webServer` target** -- Point CI at a prebuilt preview build.
232
+
233
+ ## See Also
234
+
235
+ - `ct-testing-patterns` — the canonical cross-runner test-speed rule (parallelism sized to the runner, file-level `--shard` + blob/merge-reports).
236
+ - `ct-vite-vitest-patterns` — shares the CI sharding/worker budget; don't oversubscribe a runner running both.
@@ -68,6 +68,34 @@ prost_build::Config::new()
68
68
  - Adding fields and enum values is safe.
69
69
  - Run `buf breaking --against .git#branch=main` before merging.
70
70
 
71
+ ## Performance
72
+
73
+ > _Verified against proto3 / prost / protoc-gen-ts (2026-06)._
74
+
75
+ Wire format = field tag + value. Tag size depends only on the field NUMBER (`tag = (number << 3) | wire_type`, varint-encoded), so number placement is a perf decision, not just style.
76
+
77
+ | Field number | Tag bytes |
78
+ |---|---|
79
+ | 1-15 | 1 |
80
+ | 16-2047 | 2 |
81
+ | 2048+ | 3 |
82
+
83
+ ```protobuf
84
+ // Hot message decoded in tight loops -- keep it small, flat, low numbers.
85
+ message Tick {
86
+ int64 ts = 1; // 1-byte tag
87
+ double price = 2;
88
+ repeated int32 levels = 3; // packed automatically in proto3 (one tag for the whole run)
89
+ string debug_note = 16; // rarely set -> 2-byte tag, fine up here
90
+ }
91
+ ```
92
+
93
+ - **Packed repeated scalars are free wins.** `repeated int32/int64/float/double/bool/enum` are packed by default in proto3 (single tag + length, not a tag per element). `repeated string/bytes/message` are NOT packed -- prefer packed scalar arrays over `repeated SomeWrapper` in hot paths.
94
+ - **`bytes`, not `string`, for opaque data.** `string` is UTF-8-validated on every decode (prost runs `str::from_utf8` eagerly; protoc-gen-ts runs `TextDecoder` with `fatal`). Use `bytes` for hashes, tokens, IDs, and binary blobs to skip the scan; keep `string` for human-readable text.
95
+ - **Keep hot messages flat.** Each nested sub-message is a separate length-delimited decode + allocation in prost. Inline frequent fields; reserve nesting for cold/rare data.
96
+ - **Decode once.** prost decodes the whole message eagerly and allocates as it goes -- there are no lazy fields (proto3 `[lazy=true]` is not implemented by prost or protoc-gen-ts). Decode a payload one time and pass the struct around; never re-parse the same bytes. (The one zero-copy lever: generate `bytes` fields as `bytes::Bytes` via `prost_build`'s `.bytes(...)` to borrow from the input buffer instead of copying.)
97
+ - **Route without full decode.** Cheapest dispatch/filter is a transport header carried outside the message body. If you must read one field from the payload, neither prost nor protoc-gen-ts exposes a partial-decode API -- you decode the whole message -- so put any field you might hand-scan for at number 1-15 to keep that scan short.
98
+
71
99
  ## Anti-Patterns
72
100
 
73
101
  1. **Reusing field numbers** -- Causes data corruption. Always reserve removed numbers.
@@ -76,3 +104,11 @@ prost_build::Config::new()
76
104
  4. **Proto2 syntax** -- Always proto3 for new projects.
77
105
  5. **Skipping buf lint** -- Inconsistent naming compounds. Lint early.
78
106
  6. **Large messages** -- Not for multi-MB payloads. Use streaming/chunking.
107
+ 7. **`string` for binary/opaque data** -- Forces UTF-8 validation on every decode. Use `bytes`.
108
+ 8. **`repeated` wrapper messages for scalar arrays** -- Loses proto3 packing. Use `repeated int32/double/...` directly in hot paths.
109
+ 9. **Re-decoding the same payload** -- No lazy fields; every Decode allocates (the only zero-copy path is `bytes` fields generated as `bytes::Bytes`). Decode once, reuse the struct.
110
+ 10. **High field numbers on hot fields** -- Numbers >=16 cost 2+ tag bytes per occurrence. Keep frequent fields at 1-15.
111
+
112
+ ## See Also
113
+
114
+ - `ct-rust-wasm-patterns` — the decode-once consumer side; generate `bytes` fields as `bytes::Bytes` to borrow from the input buffer.
@@ -66,6 +66,60 @@ pub struct User {
66
66
 
67
67
  Parse body: `let body: CreateUserRequest = req.json().await?;`
68
68
 
69
+ ## Performance
70
+
71
+ > _Verified against worker-rs / wasm32-unknown-unknown (2026-06)._
72
+
73
+ Two costs dominate on Workers: **.wasm size** (eats the startup CPU budget, the 10MB bundle limit, and worst-case cold starts) and the **JS<->WASM boundary** (large Request/Response bodies and serde payloads get *copied* across — not the cost of small calls, the cost of big bytes).
74
+
75
+ ### Binary size — the release profile does the heavy lifting
76
+
77
+ Most size comes from the Rust build, not `wasm-opt`. In `Cargo.toml`:
78
+
79
+ ```toml
80
+ [profile.release]
81
+ opt-level = "z" # "z" = smallest; try "s" — sometimes smaller AND faster, always measure
82
+ lto = true # cross-crate inlining + dead-code elimination
83
+ codegen-units = 1 # better optimization, slower build
84
+ strip = true # drop symbols
85
+ ```
86
+
87
+ Don't bother with `panic = "abort"` for size: `wasm32-unknown-unknown` already defaults to abort, so there are no unwind tables to drop. Modern `worker-build` recovers from panics automatically — a panicking request returns 500 and the instance reinitializes, no config needed. Keep `console_error_panic_hook` regardless — it surfaces the panic message in your logs.
88
+
89
+ `worker-build` runs `wasm-opt` automatically before upload (a balanced `-O`, not `-Oz`) — no manual step. For size-aggressive optimization, opt into `-Oz` via the `wasm-opt` key in your Cargo package metadata.
90
+
91
+ Targets: <1MB compressed is fine; smaller = leaner startup. Use `cargo bloat --release` to find the heavy deps before trimming.
92
+
93
+ ### Boundary — pass bulk data, don't re-copy it; and cut DB round-trips
94
+
95
+ The boundary cost that bites is copying *large* payloads across JS<->WASM. Deserialize the body once (`req.json().await?`), not field-by-field, and don't shuttle the same buffer back and forth.
96
+
97
+ Separately, cut **D1 round-trips** — these are network hops, not boundary copies, and they dominate latency. One call beats N:
98
+
99
+ ```rust
100
+ // SLOW: one D1 round-trip per id
101
+ for id in &ids {
102
+ db.prepare("SELECT * FROM users WHERE id=?1").bind(&[id.into()])?.first::<User>(None).await?;
103
+ }
104
+
105
+ // FAST: expand placeholders, bind every value, one round-trip
106
+ let ph = (1..=ids.len()).map(|i| format!("?{i}")).collect::<Vec<_>>().join(",");
107
+ let binds = ids.iter().map(|id| id.into()).collect::<Vec<_>>();
108
+ let users = db.prepare(format!("SELECT * FROM users WHERE id IN ({ph})"))
109
+ .bind(&binds)?.all().await?.results::<User>()?;
110
+ ```
111
+
112
+ For many independent statements in one hop, use `db.batch(vec![...])` (statements run sequentially in a single call).
113
+
114
+ Hoist binding lookups out of loops — good hygiene, cheap to do:
115
+
116
+ ```rust
117
+ let cfg = ctx.env.var("CONFIG")?.to_string(); // once, not per iteration
118
+ for row in &rows { /* use cfg */ }
119
+ ```
120
+
121
+ WASM wins on CPU-bound bulk work (parsing, crypto, number crunching) kept *inside* Rust. For trivial routing/string work the marshalling overhead can exceed the win — keep those paths thin.
122
+
69
123
  ## Anti-Patterns
70
124
 
71
125
  1. **Panics in production** -- Always `Result`, never `unwrap()`/`expect()`.
@@ -74,3 +128,11 @@ Parse body: `let body: CreateUserRequest = req.json().await?;`
74
128
  4. **Buffering large files** -- 128MB memory limit. Stream, don't buffer.
75
129
  5. **Raw SQL concatenation** -- Always `bind()` prepared statements.
76
130
  6. **Missing CORS** -- Handle OPTIONS preflight for API workers.
131
+ 7. **Cargo-culting `panic="abort"`** -- It's already the default on `wasm32-unknown-unknown` (no size win). Set the real size knobs (`opt-level="z"`, `lto=true`, `codegen-units=1`, `strip=true`); `worker-build` already auto-recovers from panics.
132
+ 8. **Chatty D1 calls** -- Per-row queries each cost a network round-trip. Collapse into one statement (expanded `IN` with bound params) or `db.batch(...)`.
133
+ 9. **WASM for trivial work** -- Boundary marshalling of small payloads can exceed the compute. Use WASM for bulk CPU work, not tiny string ops; keep large buffers from re-crossing.
134
+
135
+ ## See Also
136
+
137
+ - `ct-cloudflare-d1-kv` — same D1 round-trip batching (`IN` + bound params, `db.batch()`), documented from the binding side.
138
+ - `ct-protobuf-contracts` — decode once / `bytes` over `string`; generate `bytes` fields as `bytes::Bytes` for zero-copy.
@@ -56,6 +56,65 @@ setState("user", "settings", "theme", "light");
56
56
  setState("items", items => [...items, newItem]);
57
57
  ```
58
58
 
59
+ ## Performance
60
+
61
+ > _Verified against SolidJS 1.x (2026-06); 2.0 changes batching semantics._
62
+
63
+ Solid is fast by default: no VDOM, no re-render cycle. Wins come from list keying, splitting bundles, and keeping the reactive graph narrow.
64
+
65
+ ### `<For>` vs `<Index>`
66
+
67
+ | Use | When | Keyed by |
68
+ |-----|------|----------|
69
+ | `<For>` | array of objects; add/remove/reorder matters | item reference |
70
+ | `<Index>` | primitives, or fixed slots where only the value changes (inputs, cells) | position |
71
+
72
+ ```tsx
73
+ <For each={users()}>{(u) => <Row user={u} />}</For> // identity matters
74
+ <Index each={scores()}>{(s, i) => <Cell value={s()} />}</Index> // s is a SIGNAL: s(), i is a number
75
+ ```
76
+
77
+ Wrong choice tears down and rebuilds rows on every change. Note the swap: `<For>` gives `(item, index())`, `<Index>` gives `(item(), index)`.
78
+
79
+ ### Code-split for cold-start
80
+
81
+ Split at route/screen boundaries so the first screen isn't blocked by the whole app (critical under Capacitor).
82
+
83
+ ```tsx
84
+ const Settings = lazy(() => import("./screens/Settings"));
85
+ <Suspense fallback={<Spinner />}><Settings /></Suspense> // createResource suspends here too
86
+ ```
87
+
88
+ ### Hot paths: `batch` + `untrack`
89
+
90
+ Only writes inside a reactive computation (effect/memo) auto-batch. Writes from event handlers, timers, async callbacks, and resolved promises do NOT — wrap multiple writes in `batch()` (Solid 1.x; 2.0 will auto-batch everything).
91
+
92
+ ```tsx
93
+ const onSave = () => batch(() => { setX(1); setY(2); }); // event handler: NOT auto-batched -> batch()
94
+ setTimeout(() => batch(() => { setX(1); setY(2); }), 0); // async/timer: one update, not two
95
+ createEffect(() => { log(a()); untrack(() => b()); }); // subscribe to a, read b without subscribing
96
+ ```
97
+
98
+ ### Keep state granular
99
+
100
+ ```tsx
101
+ // Whole-object signal: every reader re-runs on ANY field change (new ref = notify)
102
+ const [user, setUser] = createSignal({ name, age });
103
+ // Store: path update notifies only that path's dependents
104
+ const [user, setUser] = createStore({ name, age });
105
+ setUser("age", 31); // name readers untouched
106
+ ```
107
+
108
+ Keep signals local; lift only what's truly shared.
109
+
110
+ ### Theme via attribute, not signal
111
+
112
+ Driving theme through a signal+context re-evaluates every consumer on toggle. Use a data-attribute + CSS vars — zero reactive work:
113
+
114
+ ```tsx
115
+ document.documentElement.dataset.theme = "dark"; // CSS: [data-theme="dark"] { --bg: #111 }
116
+ ```
117
+
59
118
  ## Anti-Patterns
60
119
 
61
120
  1. **Destructuring props** -- Breaks reactivity. Always `props.x`.
@@ -64,3 +123,14 @@ setState("items", items => [...items, newItem]);
64
123
  4. **Manual keys in For** -- `<For>` is keyed by reference automatically.
65
124
  5. **Signals outside reactive context** -- `count()` at top level captures once. Wrap in JSX or effects.
66
125
  6. **Forgetting to call signals** -- `count` is a getter, `count()` is the value.
126
+ 7. **`<Index>` for object lists / `<For>` for primitives** -- Mismatched keying rebuilds rows. Objects→`<For>`, primitives/slots→`<Index>`.
127
+ 8. **Theme/global toggles as signal+context** -- Re-evaluates all consumers. Use a `data-` attribute + CSS vars.
128
+ 9. **One signal holding a whole object** -- Any field change re-runs every reader. Use `createStore` for path-granular updates.
129
+ 10. **Eager-importing every screen** -- Blocks cold-start. `lazy()` + `<Suspense>` at route/screen boundaries.
130
+ 11. **Assuming event handlers auto-batch** -- They don't (Solid 1.x). Multiple writes in a handler/timer/async callback run dependents per-write; wrap them in `batch()`.
131
+
132
+ ## See Also
133
+
134
+ - `ct-vanilla-extract-patterns` — theme via `data-theme`/class swap (not signals); logical properties for RTL.
135
+ - `ct-i18n-typesafe` — defer locale loading behind a `createResource`-tracked `<Suspense>` (a bare `await` never trips Solid's boundary).
136
+ - `ct-capacitor-ui` — `lazy()` + `<Suspense>` screen splitting is the main lever for mobile cold-start.
@@ -51,7 +51,8 @@
51
51
  "ct-testing-patterns",
52
52
  "ct-vite-vitest-patterns",
53
53
  "ct-storybook-patterns",
54
- "ct-playwright-patterns"
54
+ "ct-playwright-patterns",
55
+ "ct-i18n-typesafe"
55
56
  ]
56
57
  }
57
58
  }
@@ -156,6 +156,58 @@ test("renders primary", () => {
156
156
  });
157
157
  ```
158
158
 
159
+ ## Performance
160
+
161
+ > _Verified against Storybook 8/9/10 + Vitest browser mode (2026-06)._
162
+
163
+ Browser-mode story tests are the slowest part of CI. Optimize for parallelism, lazy work, and caching.
164
+
165
+ ### Shard the Vitest run across CI jobs
166
+
167
+ ```bash
168
+ # CI matrix: 4 jobs, each runs 1/4 of the story FILES (sharding is file-level)
169
+ vitest run --project=storybook --shard=1/4 --reporter=blob # job 1
170
+ vitest run --project=storybook --shard=2/4 --reporter=blob # job 2 ... etc.
171
+ # final job merges results + coverage from all shards:
172
+ vitest --merge-reports --reporter=junit --coverage
173
+ ```
174
+
175
+ - `--project=storybook` skips the jsdom `unit` project when you only need stories.
176
+ - Browser tests already run files in parallel (`fileParallelism` defaults on). Don't disable it.
177
+ - `--reporter=blob` per shard + `--merge-reports` is required, or coverage is per-shard and incomplete.
178
+
179
+ ### Don't confuse `instances` with parallelism
180
+
181
+ `browser.instances` runs the **same** test files across different browsers/setups -- each extra instance re-runs the whole suite. It is not a way to add CPU parallelism.
182
+
183
+ ```typescript
184
+ browser: { enabled: true, headless: true, provider: playwright(),
185
+ instances: [{ browser: "chromium" }] }, // keep ONE for speed
186
+ // Add instances only to cover more browsers (each re-runs everything).
187
+ // Scale CPU via fileParallelism (within a job) + --shard (across CI jobs).
188
+ ```
189
+
190
+ ### Dev startup is already lazy
191
+
192
+ On-demand story loading is automatic in modern Storybook (Vite builder, code-split: a story's code loads only when rendered). There is no flag to enable -- it's simply the default in Storybook 8/9/10 (the old `storyStoreV7` flag is gone).
193
+
194
+ - Webpack's `lazyCompilation`/`fsCache` do **not** apply to the Vite builder; ignore that advice for this stack.
195
+ - Don't defeat code-splitting with eager glob imports of all stories in custom config.
196
+
197
+ ### Keep stories cheap
198
+
199
+ - Scope expensive setup **per-story**, not in `preview.ts` globals -- global decorators/loaders run for *every* story x every interaction test.
200
+ - `mswLoader` and theme providers as globals are fine if light; move heavy data setup to `parameters` on the stories that need it.
201
+ - Gate axe-core with `parameters.a11y.test` (`"error" | "todo" | "off"`), settable project -> component -> story (most specific wins). `test: "error"` globally runs the full ruleset on every story; downgrade or `"off"` where not needed, or run a11y in its own shard. (`tags` only include/exclude stories from a run -- they don't toggle a11y.)
202
+
203
+ ### Cache the Vite dep-prebundle between CI runs
204
+
205
+ ```yaml
206
+ # Cache node_modules/.vite (Vite's default cacheDir), keyed on the lockfile.
207
+ # This is the esbuild dependency pre-bundling cache used by dev / the browser-test run.
208
+ # Note: it does NOT speed up `storybook build` (vite build ignores it).
209
+ ```
210
+
159
211
  ## Anti-Patterns
160
212
 
161
213
  1. **Destructuring SolidJS props in stories** -- Pass props via `args`; use `createJSXDecorator` for decorators.
@@ -164,3 +216,13 @@ test("renders primary", () => {
164
216
  4. **Skipping error/edge-case stories** -- Always include loading, error, empty, boundary states.
165
217
  5. **Using `@storybook/test-runner`** -- Use `@storybook/addon-vitest` instead.
166
218
  6. **Registering `sb.mock` in story files** -- Register in `.storybook/preview.ts` only.
219
+ 7. **Running the full story suite in one CI job** -- Use `vitest --shard=N/M` across matrix jobs (with `--reporter=blob` + `--merge-reports`).
220
+ 8. **Duplicating same-browser `instances` to "go faster"** -- Each instance re-runs the whole suite. Scale via `fileParallelism` + `--shard`; add instances only for more browsers.
221
+ 9. **Heavy decorators/loaders in `preview.ts`** -- Global setup runs per-story; scope expensive providers/data per-story.
222
+ 10. **`a11y: { test: "error" }` globally with no scoping** -- Axe runs the full ruleset per story; downgrade per story via `parameters.a11y.test` or use a dedicated a11y shard.
223
+ 11. **Discarding the Vite cache between CI runs** -- Persist `node_modules/.vite` keyed on the lockfile (dev / browser-test speedup; not `storybook build`).
224
+
225
+ ## See Also
226
+
227
+ - `ct-testing-patterns` — the canonical cross-runner test-speed rule (file-level `--shard` + blob/merge-reports, parallelism).
228
+ - `ct-vite-vitest-patterns` — owns the deep Vitest sharding/`--project`/cache config the browser-mode run inherits.
@@ -67,6 +67,58 @@ const props = defineProperties({
67
67
  export const sprinkles = createSprinkles(props);
68
68
  ```
69
69
 
70
+ ## Performance
71
+
72
+ > _Verified against vanilla-extract (2026-06)._
73
+
74
+ Zero-runtime is the point: all classes/vars are extracted at build, the browser does the styling. Keep it that way — the wins below are about *bytes shipped* and *theme-swap cost*, not JS.
75
+
76
+ ### Theme swap = one attribute write, zero re-render
77
+
78
+ `createTheme` returns a **class that only sets CSS custom properties**. Swapping themes is one DOM write; the cascade repaints — no component re-render, no JS restyle.
79
+
80
+ ```ts
81
+ // createTheme(tokens) -> [class, contract]; createTheme(contract, tokens) -> class
82
+ export const [themeClass, vars] = createTheme({ color: { bg: '#fff', text: '#111' } });
83
+ export const dark = createTheme(vars, { color: { bg: '#111', text: '#eee' } }); // same contract, new values
84
+
85
+ // runtime: flip the class on a root element — that's the entire cost
86
+ root.className = isDark ? dark : themeClass; // no setState, no re-render
87
+ ```
88
+
89
+ Prefer the attribute path if you'd rather not own the class: register the theme on a selector at build time, then the runtime cost is a single `data-*` write.
90
+
91
+ ```ts
92
+ // build time (.css.ts): bind a theme to a selector
93
+ createGlobalTheme('[data-theme="dark"]', vars, { color: { bg: '#111', text: '#eee' } });
94
+ // runtime: one attribute write, cascade does the rest
95
+ document.documentElement.dataset.theme = 'dark';
96
+ ```
97
+
98
+ `assignVars` is the *build-time* way to assign a whole contract in one shot inside a `style()`/`globalStyle()` `vars` block — handy for declaring a theme on a selector — not a runtime knob. Never put theme values in component state; let the CSS-variable cascade do it.
99
+
100
+ ### Sprinkles dedup — CSS grows with declarations, not components
101
+
102
+ Each property-value(-condition) emits **one class, once**. 500 components calling `sprinkles({ padding: 'md' })` share a single class. Stylesheet size is bounded by `properties x values x conditions`, not usage. (Note: `style({ padding: vars.space.md })` does *not* dedupe across call sites — route high-frequency props through sprinkles to get the shared class.)
103
+
104
+ ```ts
105
+ // good: high-frequency layout/spacing -> sprinkles (deduped)
106
+ <div class={sprinkles({ display: 'flex', paddingInline: 'lg' })} />
107
+ // reserve style() for genuine one-offs
108
+ ```
109
+
110
+ Keep condition sets lean: every condition multiplies class count per property. A property with 6 values across 3 breakpoints = 18 classes.
111
+
112
+ ### Logical properties (also your RTL story)
113
+
114
+ Author `paddingInline` / `marginInline` / `insetInlineStart` instead of `left`/`right`. One stylesheet flips for RTL via `dir="rtl"` — no duplicate LTR/RTL CSS, no runtime mirroring lib.
115
+
116
+ | physical | logical |
117
+ | ------------------- | -------------------- |
118
+ | `paddingLeft/Right` | `paddingInline` |
119
+ | `marginTop/Bottom` | `marginBlock` |
120
+ | `left` / `right` | `insetInlineStart/End` |
121
+
70
122
  ## Anti-Patterns
71
123
 
72
124
  1. **Inline styles** -- Use `style()` or `sprinkles()`.
@@ -74,3 +126,13 @@ export const sprinkles = createSprinkles(props);
74
126
  3. **Overusing globalStyle** -- Prefer scoped `style()`. Reserve global for resets only.
75
127
  4. **Non-`.css.ts` files** -- Build plugin only processes `*.css.ts`.
76
128
  5. **Raw CSS variable strings** -- Use the `vars` object for type-safe references.
129
+ 6. **Theme in component state** — Re-renders the tree on toggle. Swap a class/`data-theme` on the root; the CSS-var cascade handles the rest with zero re-render.
130
+ 7. **Bespoke `style()` for common spacing/layout** — Defeats sprinkles dedup (`style()` doesn't merge identical declarations across call sites). Route high-frequency props through `sprinkles()`; one shared class instead of N.
131
+ 8. **Physical left/right** — Forces a second RTL stylesheet. Use logical `*Inline`/`*Block` props; one sheet flips with `dir="rtl"`.
132
+ 9. **Wide conditions x properties matrix** — Class count = properties x values x conditions. Trim breakpoints and property lists before they explode the CSS.
133
+
134
+ ## See Also
135
+
136
+ - `ct-solidjs-patterns` — keep theme out of the reactive graph; swap a `data-theme` attribute, not a signal.
137
+ - `ct-i18n-typesafe` — RTL = `dir` from locale (i18n) + logical properties (here).
138
+ - `ct-capacitor-ui` — mobile webview: animate `transform`/`opacity` only; avoid `box-shadow`/`filter` on scrolled nodes.
@@ -34,7 +34,7 @@
34
34
  "createTheme\\("
35
35
  ]
36
36
  },
37
- "relatedSkills": ["ct-solidjs-patterns"]
37
+ "relatedSkills": ["ct-solidjs-patterns", "ct-i18n-typesafe"]
38
38
  }
39
39
  }
40
40
  }
@@ -470,6 +470,67 @@ The compat layer auto-converts old keys. New code should use the new keys.
470
470
 
471
471
  ---
472
472
 
473
+ ## Performance
474
+
475
+ > _Verified against Vite 8 / Rolldown / Vitest 4 (2026-06)._
476
+
477
+ ### Dev Cold-Start & HMR: optimizeDeps
478
+
479
+ Vite pre-bundles bare-import deps on first run, cached in `node_modules/.vite`. A missing entry triggers a mid-session "new dependency optimized, reloading" full reload -- the #1 HMR killer.
480
+
481
+ ```typescript
482
+ optimizeDeps: {
483
+ include: ['lodash-es', 'pkg/cjs-only'], // force-bundle CJS / plugin-injected / lazy deps not auto-discovered
484
+ exclude: ['small-pure-esm-lib'], // only small pure-ESM pkgs, or deps that break when bundled
485
+ }
486
+ ```
487
+
488
+ Do **not** `exclude` a large multi-file ESM dep (e.g. `lodash-es` ships 600+ modules) -- unbundled it floods the browser with parallel requests and slows page loads; keep it pre-bundled. Never `exclude` a CJS dep. If an excluded ESM dep has a nested CJS dep, add that nested dep to `include`.
489
+
490
+ `vite --force` (or `rm -rf node_modules/.vite`) busts a stale cache. Pair with `server.warmup.clientFiles` (already covered).
491
+
492
+ ### Bundle Size: measure, then split
493
+
494
+ ```typescript
495
+ import { visualizer } from 'rollup-plugin-visualizer' // ^7 for Vite 8 / Rolldown
496
+ process.env.ANALYZE && visualizer({ gzipSize: true, brotliSize: true }) // ANALYZE=1 vite build
497
+ ```
498
+
499
+ Read the gzip/brotli column to find oversized chunks. Route-level dynamic `import()` is the primary code-split. Use manual chunks only to isolate stable vendor deps. In Vite 8 / Rolldown the `manualChunks` object form is removed and the function form is deprecated -- use `rolldownOptions.output.codeSplitting` (the old `advancedChunks` key is a deprecated alias):
500
+
501
+ ```typescript
502
+ build: { rolldownOptions: { output: {
503
+ codeSplitting: { groups: [{ name: 'vendor', test: /node_modules/ }] },
504
+ } } }
505
+ ```
506
+
507
+ ### build.target & CSS
508
+
509
+ Don't lower `target: 'baseline-widely-available'` without cause. A low target (`es2015`) makes Oxc down-level async/await, optional chaining, and spread into verbose helpers -- bigger bundles, slower transforms. Match real browser support.
510
+
511
+ ```typescript
512
+ css: { transformer: 'lightningcss' }, // experimental: faster + smaller than the postcss default
513
+ ```
514
+
515
+ `build.cssMinify` already defaults to `'lightningcss'` in Vite 8 -- no action needed for minification; the opt-in above is only for the CSS *transformer*.
516
+
517
+ ### Vitest Run Speed: pool & isolate
518
+
519
+ `isolate: true` (default) builds a fresh environment per test file -- safe but slow. Disabling it is often the single biggest speedup for pure-logic suites.
520
+
521
+ | Goal | Setting |
522
+ |---|---|
523
+ | Safe default (global mutation, `process.env`, fake timers) | `pool: 'forks'`, `isolate: true` |
524
+ | Fastest, pure side-effect-free Node logic | `pool: 'threads'`, `isolate: false` |
525
+ | Fast jsdom (leaks globals -- verify) | `pool: 'vmThreads'` (isolation can't be disabled) |
526
+ | Debug a flaky/order-dependent test | `--no-file-parallelism` or `poolOptions.threads.singleThread` |
527
+
528
+ ```typescript
529
+ test: { pool: 'threads', isolate: false } // big speedup for logic suites; leaks global state between files
530
+ ```
531
+
532
+ > `pool: 'forks'` and `isolate: true` are the Vitest defaults. Gains from `threads` show mostly in larger suites; `forks` stays safer for native-module / segfault-prone tests.
533
+
473
534
  ## Anti-Patterns
474
535
 
475
536
  ### Vite
@@ -479,6 +540,10 @@ The compat layer auto-converts old keys. New code should use the new keys.
479
540
  3. **Duplicating resolve config for tests** -- Vitest inherits Vite's aliases and plugins. Don't redeclare.
480
541
  4. **Using `rollupOptions` in Vite 8** -- Works via compat but generates warnings. Use `rolldownOptions`.
481
542
  5. **Not externalizing peer deps in library mode** -- Bundling framework deps causes duplicate instances.
543
+ 6. **Lowering `build.target` by habit** -- Down-levels modern syntax into helper bloat and slows transforms. Set it to your actual browser support.
544
+ 7. **Tuning chunks without measuring** -- Run `ANALYZE=1 vite build` with `visualizer` first. Over-grouping into manual chunks defeats per-route lazy loading and cache granularity.
545
+ 8. **`optimizeDeps.exclude` on large ESM deps** -- Unbundling a multi-file ESM package floods the browser with requests. Exclude only small pure-ESM or bundle-breaking deps.
546
+ 9. **Ignoring the "new dependency optimized, reloading" warning** -- A dep escaped pre-bundle discovery. Add it to `optimizeDeps.include` to stop full reloads.
482
547
 
483
548
  ### Vitest
484
549
 
@@ -490,3 +555,9 @@ The compat layer auto-converts old keys. New code should use the new keys.
490
555
  6. **Snapshot-only testing** -- Snapshots catch regressions but don't verify correctness. Add explicit assertions.
491
556
  7. **Global jsdom environment** -- Use `test.projects` to run DOM tests in jsdom and logic tests in node.
492
557
  8. **Using `vi.spyOn` in browser mode** -- Use `vi.mock('./module', { spy: true })` instead.
558
+ 9. **Leaving `isolate: true` on pure-logic suites** -- A large run-speed cost. Set `isolate: false` + `pool: 'threads'` for side-effect-free tests; keep `forks` + `isolate: true` where global/env mutation needs isolation.
559
+
560
+ ## See Also
561
+
562
+ - `ct-testing-patterns` — the canonical cross-runner test-speed rule (parallelism, file-level `--shard` + blob/merge-reports, isolate trade-off).
563
+ - `ct-solidjs-patterns` — `lazy()` route/screen code-splitting relies on Vite's dynamic-import chunking.