claude-toolkit 0.1.25 → 0.9.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/CHANGELOG.md +42 -0
- package/README.md +7 -6
- package/core/hooks/skill-eval.js +3 -20
- package/core/skills/ct-testing-patterns/SKILL.md +12 -0
- package/package.json +1 -1
- package/src/detect.ts +204 -71
- package/stacks/capacitor/skills/ct-capacitor-ota/SKILL.md +16 -0
- package/stacks/capacitor/skills/ct-capacitor-ui/SKILL.md +66 -0
- package/stacks/capacitor/stack.json +47 -1
- package/stacks/cloudflare/skills/ct-cloudflare-d1-kv/SKILL.md +86 -0
- package/stacks/i18n-typesafe/skills/ct-i18n-typesafe/SKILL.md +52 -0
- package/stacks/i18n-typesafe/stack.json +2 -1
- package/stacks/playwright/skills/ct-playwright-patterns/SKILL.md +70 -2
- package/stacks/protobuf/skills/ct-protobuf-contracts/SKILL.md +36 -0
- package/stacks/rust-wasm/skills/ct-rust-wasm-patterns/SKILL.md +62 -0
- package/stacks/solidjs/skills/ct-solidjs-patterns/SKILL.md +70 -0
- package/stacks/solidjs/stack.json +2 -1
- package/stacks/storybook/skills/ct-storybook-patterns/SKILL.md +62 -0
- package/stacks/vanilla-extract/skills/ct-vanilla-extract-patterns/SKILL.md +62 -0
- package/stacks/vanilla-extract/stack.json +1 -1
- package/stacks/vite/skills/ct-vite-vitest-patterns/SKILL.md +71 -0
|
@@ -96,6 +96,83 @@ Key points:
|
|
|
96
96
|
- Always use Hyperdrive for external database connections from Workers.
|
|
97
97
|
- Connection pool size is configurable per Hyperdrive config.
|
|
98
98
|
|
|
99
|
+
## Performance
|
|
100
|
+
|
|
101
|
+
> _Verified against Cloudflare Workers / D1 / KV (2026-06)._
|
|
102
|
+
|
|
103
|
+
Latency on Workers is dominated by **network round trips**, not CPU. Every `await` on D1/KV/`fetch` is a hop -- collapse and parallelize them.
|
|
104
|
+
|
|
105
|
+
### Don't serialize independent round trips
|
|
106
|
+
|
|
107
|
+
```rust
|
|
108
|
+
// BAD: ~2x latency -- two independent reads run back to back
|
|
109
|
+
let user = get_user(&db, id).await?;
|
|
110
|
+
let prefs = get_prefs(&kv, id).await?;
|
|
111
|
+
|
|
112
|
+
// GOOD: one round-trip wall-time -- run concurrently
|
|
113
|
+
let (user, prefs) = futures::try_join!(get_user(&db, id), get_prefs(&kv, id))?;
|
|
114
|
+
```
|
|
115
|
+
```typescript
|
|
116
|
+
// TS equivalent
|
|
117
|
+
const [user, prefs] = await Promise.all([getUser(db, id), getPrefs(kv, id)]);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
I/O overlaps in the single-threaded isolate, so wall-time approaches the slowest call. (A Worker can have only ~6 connections waiting on response headers at once -- so fan-out is no substitute for collapsing N reads into one query.)
|
|
121
|
+
|
|
122
|
+
Kill N+1: never loop per-row queries. Use one `IN (...)`/JOIN, or `db.batch()` of selects.
|
|
123
|
+
|
|
124
|
+
```rust
|
|
125
|
+
// BAD: 1 + N round trips
|
|
126
|
+
for id in ids { rows.push(db.prepare("SELECT * FROM u WHERE id=?1").bind(&[id.into()])?.first(None).await?); }
|
|
127
|
+
// GOOD: 1 round trip
|
|
128
|
+
let placeholders = ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
|
|
129
|
+
let users = db.prepare(&format!("SELECT * FROM u WHERE id IN ({placeholders})"))
|
|
130
|
+
.bind(&binds)?.all().await?.results::<User>()?; // .all() -> D1Result; .results() -> typed rows
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Verify the index is actually used
|
|
134
|
+
|
|
135
|
+
An index exists != the planner uses it. Functions on columns, leading-`%` LIKE, and TEXT/INT mismatches force a SCAN.
|
|
136
|
+
|
|
137
|
+
```sql
|
|
138
|
+
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?1;
|
|
139
|
+
-- want: SEARCH users USING INDEX idx_users_email (NOT: SCAN users)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Cache API for full responses (not just KV)
|
|
143
|
+
|
|
144
|
+
`caches.default` is data-center-local (contents don't replicate across colos): sub-ms in-colo, cheaper than a KV read, no eventual-consistency lag. Use it for whole GET responses (only GET is cacheable); use KV read-through for sub-response values. Fill the cache off the response path with `waitUntil`.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const cache = caches.default;
|
|
148
|
+
let res = await cache.match(request);
|
|
149
|
+
if (!res) {
|
|
150
|
+
res = await render(request); // Cache-Control sets TTL
|
|
151
|
+
ctx.waitUntil(cache.put(request, res.clone())); // non-blocking write
|
|
152
|
+
}
|
|
153
|
+
return res;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`cache.put` rejects non-GET, `206`, `Vary: *`, and `Set-Cookie` responses (strip the cookie or set `Cache-Control: private=Set-Cookie` first).
|
|
157
|
+
|
|
158
|
+
Same rule for cache/KV writes and invalidation: `ctx.waitUntil(kv.put(...))` so writes never add to TTFB.
|
|
159
|
+
|
|
160
|
+
### Stream instead of buffering
|
|
161
|
+
|
|
162
|
+
Buffering the full body adds total generation time to TTFB. Return a `ReadableStream` (or pipe an upstream body) so bytes flush as produced.
|
|
163
|
+
|
|
164
|
+
### Smart Placement for origin/Hyperdrive-heavy Workers
|
|
165
|
+
|
|
166
|
+
Smart Placement moves the isolate closer to your **back-end** (origin APIs, or an external Postgres/MySQL behind Hyperdrive) when latency is dominated by multiple **sequential** calls to it:
|
|
167
|
+
|
|
168
|
+
```toml
|
|
169
|
+
# wrangler.toml
|
|
170
|
+
[placement]
|
|
171
|
+
mode = "smart"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
It does **not** help D1 or KV: D1 routing is governed by the primary-instance location + read replication (use the Sessions API to read from a nearby replica), and KV is already served from the data center the Worker runs in. Leave Smart Placement off for single-hop, D1/KV-only, or static-heavy Workers.
|
|
175
|
+
|
|
99
176
|
## Anti-Patterns
|
|
100
177
|
|
|
101
178
|
1. **Unbounded queries** -- Always LIMIT. D1 has 5MB response cap.
|
|
@@ -104,3 +181,12 @@ Key points:
|
|
|
104
181
|
4. **Skipping batch** -- Each D1 call is a network round trip.
|
|
105
182
|
5. **KV without TTL** -- Stale data persists indefinitely.
|
|
106
183
|
6. **Untested migrations** -- Always `--local` before `--remote`.
|
|
184
|
+
7. **Sequential awaits on independent reads** -- each is a round trip; use `try_join!`/`Promise.all`.
|
|
185
|
+
8. **Per-row query loops (N+1)** -- batch into one `IN (...)`/JOIN or `db.batch()`.
|
|
186
|
+
9. **Awaiting cache/KV writes before responding** -- use `ctx.waitUntil()` so puts don't add to TTFB.
|
|
187
|
+
10. **Assuming the index is used** -- check `EXPLAIN QUERY PLAN` for `USING INDEX`, not `SCAN`.
|
|
188
|
+
11. **Smart Placement for D1/KV latency** -- it targets origins/Hyperdrive, not D1/KV; use D1 read replication (Sessions API) instead.
|
|
189
|
+
|
|
190
|
+
## See Also
|
|
191
|
+
|
|
192
|
+
- `ct-rust-wasm-patterns` — the same D1 round-trip / `IN`-with-bound-params batching, from the Rust handler side.
|
|
@@ -54,6 +54,49 @@ Types are inferred from base locale: `{name:string}`, `{count:number}`. TypeScri
|
|
|
54
54
|
|
|
55
55
|
Organize by feature (common, auth, events), one level deep. Avoid deep nesting -- it makes keys verbose.
|
|
56
56
|
|
|
57
|
+
## Performance
|
|
58
|
+
|
|
59
|
+
> _Verified against typesafe-i18n + SolidJS (2026-06)._
|
|
60
|
+
|
|
61
|
+
Ship one locale, not all. typesafe-i18n generates both loaders -- pick the async one.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// CORRECT: per-locale dynamic import -> separate chunk, only active locale fetched
|
|
65
|
+
import { loadLocaleAsync } from './i18n/i18n-util.async';
|
|
66
|
+
await loadLocaleAsync(detectLocale()); // e.g. 'en' loads the i18n/en chunk only
|
|
67
|
+
|
|
68
|
+
// WRONG: i18n-util.sync static-imports every dictionary at module top -> all in this bundle
|
|
69
|
+
import { loadLocale } from './i18n/i18n-util.sync';
|
|
70
|
+
loadLocale('en'); // loadAllLocales() is worse
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Build formatters once per locale; never construct Intl objects in a component or list row. (Measured: constructing `Intl.NumberFormat` ~20x slower than reusing one instance.)
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// src/i18n/formatters.ts -- constructed once via loadFormatters(locale)
|
|
77
|
+
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale) => ({
|
|
78
|
+
// Intl.* is expensive to construct; reuse the instance across all calls
|
|
79
|
+
currency: (v: number) => new Intl.NumberFormat(locale, { style: 'currency', currency: 'EUR' }).format(v),
|
|
80
|
+
});
|
|
81
|
+
// usage in en/index.ts: price: 'Total: {amount:number|currency}' -- typed param, not new Intl.* at the call site
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Don't block first paint on the locale fetch. In Solid, `<Suspense>` only suspends on a tracked resource read -- a bare `await loadLocaleAsync()` will NOT trip the boundary. Drive it off a resource (or use the Solid adapter's `<TypesafeI18n>`, which withholds children until the locale loads):
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { createResource, Suspense } from 'solid-js';
|
|
88
|
+
// loaded() is read inside the boundary, so Suspense shows the shell until the chunk arrives
|
|
89
|
+
const [loaded] = createResource(detectLocale, loadLocaleAsync);
|
|
90
|
+
<Suspense fallback={<AppShell />}>{loaded.state === 'ready' && props.children}</Suspense>
|
|
91
|
+
// index.html: a modulepreload link for /assets/i18n-en-*.js overlaps with the main bundle fetch
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
RTL: drive the html dir attribute from the locale; use CSS logical properties so layout flips with zero extra CSS or JS.
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
.card { margin-inline-start: 1rem; padding-inline: 1rem; inset-inline-start: 0; } /* not margin-left */
|
|
98
|
+
```
|
|
99
|
+
|
|
57
100
|
## Anti-Patterns
|
|
58
101
|
|
|
59
102
|
1. **Hardcoded strings** -- Every user-visible string goes through `LL()`.
|
|
@@ -62,3 +105,12 @@ Organize by feature (common, auth, events), one level deep. Avoid deep nesting -
|
|
|
62
105
|
4. **Dynamic key access** -- `LL()[dynamicKey]()` bypasses type safety. Use conditional rendering.
|
|
63
106
|
5. **Forgetting loadLocale** -- Must call before rendering or get runtime errors.
|
|
64
107
|
6. **Over-splitting namespaces** -- Group by feature, not by component.
|
|
108
|
+
7. **Sync `loadLocale` / `loadAllLocales` in app code** -- `i18n-util.sync` static-imports every dictionary into the bundle that pulls it in. Use `loadLocaleAsync` so each locale is its own chunk.
|
|
109
|
+
8. **Constructing `Intl.NumberFormat`/`DateTimeFormat` per render** -- ~20x costlier than reuse; build once in `formatters.ts` and reference via `{v:number|formatter}`.
|
|
110
|
+
9. **Awaiting `loadLocaleAsync` without a Suspense-tracked resource** -- Blanks the screen until the JSON loads, and a bare `await` never trips Solid's `<Suspense>`. Gate via `createResource` (or `<TypesafeI18n>`), show a shell fallback, and modulepreload the active locale.
|
|
111
|
+
10. **Physical CSS for layout (`margin-left`, `left`)** -- Breaks RTL and needs per-dir overrides. Use logical properties (`margin-inline-start`, `inset-inline-start`).
|
|
112
|
+
|
|
113
|
+
## See Also
|
|
114
|
+
|
|
115
|
+
- `ct-solidjs-patterns` — the `createResource`-tracked `<Suspense>` boundary (a bare `await` never suspends in Solid) and `lazy()` cold-start splitting.
|
|
116
|
+
- `ct-vanilla-extract-patterns` — RTL = `dir` from locale (here) + logical properties authored in `.css.ts` (there).
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
@@ -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.
|