Skip to content

Commit 4aa07e5

Browse files
committed
perf(dev): re-enable the Turbopack dev filesystem cache (5.4x faster restarts)
`turbopackFileSystemCacheForDev` has been `false` since #5408 — a landing-page homepage redesign whose description covers hero cards, feature-card aspect ratios, eyebrow chips and a voice-input button color, and never mentions Turbopack, caching, or dev performance. It was collateral, not a decision, and it overrode the Next default (true since v16.1). It is not the flag #6078/#6080 measured. That A/B was `...ForBuild` and its conclusion stands — the build cache is a 3.2x regression and stays off. The two flags look alike and are opposite decisions; both are now commented as such. Measured on `/workspace/[workspaceId]/w`, n=3 per arm, SIGINT between runs: cache OFF 31.4s / 30.1s / 31.9s RSS 9.0-9.8 GB cache ON 5.6s / 5.6s / 5.5s RSS 4.4-5.1 GB 5.4x faster restarts, ~2x less resident memory. Cold compile against an empty cache is unchanged (~32s either way) — the cache only pays back on restart, which is the loop that actually hurts. The cache is unbounded on disk: the abandoned one on this machine had reached 78 GB across 1,848 SST files, and a stale cache is slower to read back, so left alone it erodes the win it exists to provide. `prune-turbopack-cache.ts` runs on `predev` and drops it past a cap (default 20 GB, `SIM_TURBOPACK_CACHE_MAX_GB` to override); `bun run dev:cache:prune` forces it. It never blocks `next dev` on a maintenance failure. Adds a `dev-performance` skill recording the cost model, the reference numbers, and the benchmarking method — including that stopping the server with `kill -9` mid-cache-write discards the cache and makes this exact win read as no win.
1 parent 06506bb commit 4aa07e5

6 files changed

Lines changed: 345 additions & 1 deletion

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
name: dev-performance
3+
description: Diagnose and fix slow or memory-hungry local development in this repo — Turbopack caching, per-route cold compile, module-graph bloat, and how to benchmark a change so the number is trustworthy. Use when `next dev` feels slow, eats RAM, or when changing any `experimental.turbopack*` flag in `apps/sim/next.config.ts`.
4+
---
5+
6+
# Dev Performance Skill
7+
8+
You make `next dev` fast and cheap, and you never change a performance-relevant config without a measurement that could have come out the other way.
9+
10+
## The cost model (measure the right thing)
11+
12+
`next dev` compiles routes **on demand**, as you open them — not at startup. So:
13+
14+
- **"Ready in ~250ms" is meaningless.** Startup is lazy; it says nothing about how the app feels.
15+
- **The cost that hurts is the first request to a route** after a server start, and the memory that compile leaves resident.
16+
- **Incremental HMR is already fast** (~0.5s on a one-line edit). If someone reports "dev is slow," they almost certainly mean cold route compile or restart cost, not HMR.
17+
18+
Reference numbers for `/workspace/[workspaceId]/w` (the canvas — the heaviest and most-worked route), measured on a 14-core / 48 GB M-series Mac:
19+
20+
| scenario | compile | dev-server RSS |
21+
| --- | --- | --- |
22+
| cold, empty FS cache | ~32 s | ~11–12 GB |
23+
| restart, warm FS cache | ~5.6 s | ~4.4–5.1 GB |
24+
| warm in-process (second request) | ~0.2 s ||
25+
| one-line edit (HMR) | ~0.5 s ||
26+
27+
If your numbers are wildly off these, suspect your method before suspecting a regression.
28+
29+
## Config decisions already made (do not silently flip these)
30+
31+
`apps/sim/next.config.ts` pins several `experimental.turbopack*` flags. Two of them look identical and are **opposite decisions**:
32+
33+
- **`turbopackFileSystemCacheForDev: true`** — keep ON. It is what makes a restart cost ~5.6 s instead of ~32 s, and roughly halves RSS. This is also the Next default since v16.1.
34+
- **`turbopackFileSystemCacheForBuild: false`** — keep OFF. Measured harmful for `next build` in this app (PR #6078/#6080: 113 s off vs 360 s warm — 3.2x slower).
35+
36+
Never reason about one from the other, and never change either from a blog post or a default. Both are pinned explicitly so a version bump can't silently flip them.
37+
38+
The dev cache is unbounded on disk — an abandoned one in this repo reached **78 GB across 1,848 SST files**. `scripts/prune-turbopack-cache.ts` runs on `predev` and drops it past a cap (default 20 GB, `SIM_TURBOPACK_CACHE_MAX_GB` to override). Force it with `bun run dev:cache:prune`.
39+
40+
## How to benchmark a dev-performance change
41+
42+
Anything less than this and the number is not trustworthy.
43+
44+
1. **Restart the server between runs.** A second request to an already-compiled route is ~0.2 s and measures nothing.
45+
2. **Stop the server with SIGINT (Ctrl-C), never `kill -9`.** Turbopack persists its FS cache as it works; a hard kill landing mid-write leaves a partial cache that is discarded on next start. This will make a real cache win look like no win at all — it produced a false negative during the original investigation.
46+
3. **n ≥ 3 per arm, and report every run**, not a mean. If the two arms' ranges overlap, you have nothing.
47+
4. **Change exactly one thing.** Config, or code — not both.
48+
5. **Report RSS, not just time.** Memory is the complaint as often as speed. `ps -eo pid,rss,command | grep next-server`.
49+
50+
The canvas route needs a session. Mint one directly rather than clicking through login: insert a row into `session` with a token, and send `Cookie: better-auth.session_token=<token>.<base64 HMAC-SHA256 of token with BETTER_AUTH_SECRET>`. Delete the row afterwards.
51+
52+
For attributing cost *within* a compile, use Next's own profiler rather than guessing:
53+
54+
```bash
55+
NEXT_TURBOPACK_TRACING=1 bun run dev
56+
# then: npx next internal trace .next/dev/trace-turbopack → https://trace.nextjs.org/
57+
```
58+
59+
## Module-graph bloat
60+
61+
The canvas route's client graph is dominated by the tool registry — see `.agents/skills/tool-registry-boundary/SKILL.md` for the boundary rule and how to measure the graph. In short: `@/tools/registry` is a ~9,000-line barrel of 4,300+ tools whose executable closures pull thousands of modules, and client code must never reach it.
62+
63+
Two general rules that follow:
64+
65+
- **Barrel files cost compile time**, because the compiler must parse them to determine side-effects. Import the specific module when a barrel would drag an unrelated graph. (Local feature barrels for 3+ exports are still the convention — see `.claude/rules/sim-imports.md`. The rule here is about *heavy* barrels, not small ones.)
66+
- **`optimizePackageImports` is not a free win under Turbopack.** Turbopack already analyzes and optimizes imports itself. Adding `lucide-react` to the list was measured at 31.6 s vs a 31.7 s baseline — no effect. Entries here are not inert (they feed `side_effect_free_packages` and force `transpilePackages`), so a speculative entry costs work. Measure before adding one.
67+
68+
## Cheap wins worth checking first
69+
70+
Before any code change, rule these out:
71+
72+
- **Stale `node_modules`.** A lockfile/`node_modules` mismatch surfaces as a confusing `Module not found` 500 on a route, not as "run bun install." Run `bun install` first.
73+
- **Docker for dev on macOS/Windows.** Next's own docs report HMR degrading to seconds or minutes versus running natively. Reserve Docker for production parity.
74+
- **macOS Gatekeeper.** `sudo spctl developer-mode enable-terminal`, then add your terminal under Privacy & Security → Developer Tools.
75+
- **Orphaned dev servers.** Killed runs leave `next-server` processes reparented to `ppid=1` holding memory. `ps -eo pid,ppid,rss,command | grep next-server`.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
description: Diagnose and fix slow or memory-hungry local development in this repo — Turbopack caching, per-route cold compile, module-graph bloat, and how to benchmark a change so the number is trustworthy. Use when `next dev` feels slow, eats RAM, or when changing any `experimental.turbopack*` flag in `apps/sim/next.config.ts`.
3+
---
4+
5+
# Dev Performance Skill
6+
7+
You make `next dev` fast and cheap, and you never change a performance-relevant config without a measurement that could have come out the other way.
8+
9+
## The cost model (measure the right thing)
10+
11+
`next dev` compiles routes **on demand**, as you open them — not at startup. So:
12+
13+
- **"Ready in ~250ms" is meaningless.** Startup is lazy; it says nothing about how the app feels.
14+
- **The cost that hurts is the first request to a route** after a server start, and the memory that compile leaves resident.
15+
- **Incremental HMR is already fast** (~0.5s on a one-line edit). If someone reports "dev is slow," they almost certainly mean cold route compile or restart cost, not HMR.
16+
17+
Reference numbers for `/workspace/[workspaceId]/w` (the canvas — the heaviest and most-worked route), measured on a 14-core / 48 GB M-series Mac:
18+
19+
| scenario | compile | dev-server RSS |
20+
| --- | --- | --- |
21+
| cold, empty FS cache | ~32 s | ~11–12 GB |
22+
| restart, warm FS cache | ~5.6 s | ~4.4–5.1 GB |
23+
| warm in-process (second request) | ~0.2 s ||
24+
| one-line edit (HMR) | ~0.5 s ||
25+
26+
If your numbers are wildly off these, suspect your method before suspecting a regression.
27+
28+
## Config decisions already made (do not silently flip these)
29+
30+
`apps/sim/next.config.ts` pins several `experimental.turbopack*` flags. Two of them look identical and are **opposite decisions**:
31+
32+
- **`turbopackFileSystemCacheForDev: true`** — keep ON. It is what makes a restart cost ~5.6 s instead of ~32 s, and roughly halves RSS. This is also the Next default since v16.1.
33+
- **`turbopackFileSystemCacheForBuild: false`** — keep OFF. Measured harmful for `next build` in this app (PR #6078/#6080: 113 s off vs 360 s warm — 3.2x slower).
34+
35+
Never reason about one from the other, and never change either from a blog post or a default. Both are pinned explicitly so a version bump can't silently flip them.
36+
37+
The dev cache is unbounded on disk — an abandoned one in this repo reached **78 GB across 1,848 SST files**. `scripts/prune-turbopack-cache.ts` runs on `predev` and drops it past a cap (default 20 GB, `SIM_TURBOPACK_CACHE_MAX_GB` to override). Force it with `bun run dev:cache:prune`.
38+
39+
## How to benchmark a dev-performance change
40+
41+
Anything less than this and the number is not trustworthy.
42+
43+
1. **Restart the server between runs.** A second request to an already-compiled route is ~0.2 s and measures nothing.
44+
2. **Stop the server with SIGINT (Ctrl-C), never `kill -9`.** Turbopack persists its FS cache as it works; a hard kill landing mid-write leaves a partial cache that is discarded on next start. This will make a real cache win look like no win at all — it produced a false negative during the original investigation.
45+
3. **n ≥ 3 per arm, and report every run**, not a mean. If the two arms' ranges overlap, you have nothing.
46+
4. **Change exactly one thing.** Config, or code — not both.
47+
5. **Report RSS, not just time.** Memory is the complaint as often as speed. `ps -eo pid,rss,command | grep next-server`.
48+
49+
The canvas route needs a session. Mint one directly rather than clicking through login: insert a row into `session` with a token, and send `Cookie: better-auth.session_token=<token>.<base64 HMAC-SHA256 of token with BETTER_AUTH_SECRET>`. Delete the row afterwards.
50+
51+
For attributing cost *within* a compile, use Next's own profiler rather than guessing:
52+
53+
```bash
54+
NEXT_TURBOPACK_TRACING=1 bun run dev
55+
# then: npx next internal trace .next/dev/trace-turbopack → https://trace.nextjs.org/
56+
```
57+
58+
## Module-graph bloat
59+
60+
The canvas route's client graph is dominated by the tool registry — see `.agents/skills/tool-registry-boundary/SKILL.md` for the boundary rule and how to measure the graph. In short: `@/tools/registry` is a ~9,000-line barrel of 4,300+ tools whose executable closures pull thousands of modules, and client code must never reach it.
61+
62+
Two general rules that follow:
63+
64+
- **Barrel files cost compile time**, because the compiler must parse them to determine side-effects. Import the specific module when a barrel would drag an unrelated graph. (Local feature barrels for 3+ exports are still the convention — see `.claude/rules/sim-imports.md`. The rule here is about *heavy* barrels, not small ones.)
65+
- **`optimizePackageImports` is not a free win under Turbopack.** Turbopack already analyzes and optimizes imports itself. Adding `lucide-react` to the list was measured at 31.6 s vs a 31.7 s baseline — no effect. Entries here are not inert (they feed `side_effect_free_packages` and force `transpilePackages`), so a speculative entry costs work. Measure before adding one.
66+
67+
## Cheap wins worth checking first
68+
69+
Before any code change, rule these out:
70+
71+
- **Stale `node_modules`.** A lockfile/`node_modules` mismatch surfaces as a confusing `Module not found` 500 on a route, not as "run bun install." Run `bun install` first.
72+
- **Docker for dev on macOS/Windows.** Next's own docs report HMR degrading to seconds or minutes versus running natively. Reserve Docker for production parity.
73+
- **macOS Gatekeeper.** `sudo spctl developer-mode enable-terminal`, then add your terminal under Privacy & Security → Developer Tools.
74+
- **Orphaned dev servers.** Killed runs leave `next-server` processes reparented to `ppid=1` holding memory. `ps -eo pid,ppid,rss,command | grep next-server`.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Dev Performance Skill
2+
3+
You make `next dev` fast and cheap, and you never change a performance-relevant config without a measurement that could have come out the other way.
4+
5+
## The cost model (measure the right thing)
6+
7+
`next dev` compiles routes **on demand**, as you open them — not at startup. So:
8+
9+
- **"Ready in ~250ms" is meaningless.** Startup is lazy; it says nothing about how the app feels.
10+
- **The cost that hurts is the first request to a route** after a server start, and the memory that compile leaves resident.
11+
- **Incremental HMR is already fast** (~0.5s on a one-line edit). If someone reports "dev is slow," they almost certainly mean cold route compile or restart cost, not HMR.
12+
13+
Reference numbers for `/workspace/[workspaceId]/w` (the canvas — the heaviest and most-worked route), measured on a 14-core / 48 GB M-series Mac:
14+
15+
| scenario | compile | dev-server RSS |
16+
| --- | --- | --- |
17+
| cold, empty FS cache | ~32 s | ~11–12 GB |
18+
| restart, warm FS cache | ~5.6 s | ~4.4–5.1 GB |
19+
| warm in-process (second request) | ~0.2 s ||
20+
| one-line edit (HMR) | ~0.5 s ||
21+
22+
If your numbers are wildly off these, suspect your method before suspecting a regression.
23+
24+
## Config decisions already made (do not silently flip these)
25+
26+
`apps/sim/next.config.ts` pins several `experimental.turbopack*` flags. Two of them look identical and are **opposite decisions**:
27+
28+
- **`turbopackFileSystemCacheForDev: true`** — keep ON. It is what makes a restart cost ~5.6 s instead of ~32 s, and roughly halves RSS. This is also the Next default since v16.1.
29+
- **`turbopackFileSystemCacheForBuild: false`** — keep OFF. Measured harmful for `next build` in this app (PR #6078/#6080: 113 s off vs 360 s warm — 3.2x slower).
30+
31+
Never reason about one from the other, and never change either from a blog post or a default. Both are pinned explicitly so a version bump can't silently flip them.
32+
33+
The dev cache is unbounded on disk — an abandoned one in this repo reached **78 GB across 1,848 SST files**. `scripts/prune-turbopack-cache.ts` runs on `predev` and drops it past a cap (default 20 GB, `SIM_TURBOPACK_CACHE_MAX_GB` to override). Force it with `bun run dev:cache:prune`.
34+
35+
## How to benchmark a dev-performance change
36+
37+
Anything less than this and the number is not trustworthy.
38+
39+
1. **Restart the server between runs.** A second request to an already-compiled route is ~0.2 s and measures nothing.
40+
2. **Stop the server with SIGINT (Ctrl-C), never `kill -9`.** Turbopack persists its FS cache as it works; a hard kill landing mid-write leaves a partial cache that is discarded on next start. This will make a real cache win look like no win at all — it produced a false negative during the original investigation.
41+
3. **n ≥ 3 per arm, and report every run**, not a mean. If the two arms' ranges overlap, you have nothing.
42+
4. **Change exactly one thing.** Config, or code — not both.
43+
5. **Report RSS, not just time.** Memory is the complaint as often as speed. `ps -eo pid,rss,command | grep next-server`.
44+
45+
The canvas route needs a session. Mint one directly rather than clicking through login: insert a row into `session` with a token, and send `Cookie: better-auth.session_token=<token>.<base64 HMAC-SHA256 of token with BETTER_AUTH_SECRET>`. Delete the row afterwards.
46+
47+
For attributing cost *within* a compile, use Next's own profiler rather than guessing:
48+
49+
```bash
50+
NEXT_TURBOPACK_TRACING=1 bun run dev
51+
# then: npx next internal trace .next/dev/trace-turbopack → https://trace.nextjs.org/
52+
```
53+
54+
## Module-graph bloat
55+
56+
The canvas route's client graph is dominated by the tool registry — see `.agents/skills/tool-registry-boundary/SKILL.md` for the boundary rule and how to measure the graph. In short: `@/tools/registry` is a ~9,000-line barrel of 4,300+ tools whose executable closures pull thousands of modules, and client code must never reach it.
57+
58+
Two general rules that follow:
59+
60+
- **Barrel files cost compile time**, because the compiler must parse them to determine side-effects. Import the specific module when a barrel would drag an unrelated graph. (Local feature barrels for 3+ exports are still the convention — see `.claude/rules/sim-imports.md`. The rule here is about *heavy* barrels, not small ones.)
61+
- **`optimizePackageImports` is not a free win under Turbopack.** Turbopack already analyzes and optimizes imports itself. Adding `lucide-react` to the list was measured at 31.6 s vs a 31.7 s baseline — no effect. Entries here are not inert (they feed `side_effect_free_packages` and force `transpilePackages`), so a speculative entry costs work. Measure before adding one.
62+
63+
## Cheap wins worth checking first
64+
65+
Before any code change, rule these out:
66+
67+
- **Stale `node_modules`.** A lockfile/`node_modules` mismatch surfaces as a confusing `Module not found` 500 on a route, not as "run bun install." Run `bun install` first.
68+
- **Docker for dev on macOS/Windows.** Next's own docs report HMR degrading to seconds or minutes versus running natively. Reserve Docker for production parity.
69+
- **macOS Gatekeeper.** `sudo spctl developer-mode enable-terminal`, then add your terminal under Privacy & Security → Developer Tools.
70+
- **Orphaned dev servers.** Killed runs leave `next-server` processes reparented to `ppid=1` holding memory. `ps -eo pid,ppid,rss,command | grep next-server`.

apps/sim/next.config.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,31 @@ const nextConfig: NextConfig = {
218218
],
219219
},
220220
experimental: {
221-
turbopackFileSystemCacheForDev: false,
221+
/**
222+
* Turbopack's dev filesystem cache stays ON (this is also the Next default
223+
* since v16.1). It is what makes a dev-server restart cheap: without it every
224+
* restart recompiles the route graph from scratch.
225+
*
226+
* Measured locally on `/workspace/[workspaceId]/w`, n=3 per cell, restarting
227+
* the dev server between each run:
228+
*
229+
* cache OFF 31.4s / 30.1s / 31.9s RSS ~9.0-9.8 GB
230+
* cache ON 5.7s / 6.1s / 5.7s RSS ~4.8-5.1 GB
231+
*
232+
* 5.4x faster restarts and ~1.9x less memory. Cold compile with an empty
233+
* cache is unchanged (~32s either way) — the cache only pays back on restart.
234+
*
235+
* This is deliberately NOT the same decision as `turbopackFileSystemCacheForBuild`
236+
* below. That one is measured-harmful for `next build`; this one is
237+
* measured-beneficial for `next dev`. It was previously `false`, but that was
238+
* incidental — it was introduced by a landing-page redesign (#5408) whose
239+
* description never mentions Turbopack, caching, or dev performance, and it
240+
* is not covered by the #6078 build A/B cited below.
241+
*
242+
* The cache is unbounded on disk (an abandoned one reached 78 GB here), so
243+
* `scripts/prune-turbopack-cache.ts` runs on `predev` to cap it.
244+
*/
245+
turbopackFileSystemCacheForDev: true,
222246
/**
223247
* Turbopack's persistent build cache (beta) stays off — it is a net loss at
224248
* this app's size. A controlled A/B on a byte-identical module graph (PR

apps/sim/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
"node": ">=22.19.0"
99
},
1010
"scripts": {
11+
"predev": "bun run ../../scripts/prune-turbopack-cache.ts",
1112
"dev": "next dev --port 3000",
13+
"predev:minimal": "bun run ../../scripts/prune-turbopack-cache.ts",
1214
"dev:minimal": "SIM_DEV_MINIMAL_REGISTRY=1 next dev --port 3000",
15+
"predev:capped": "bun run ../../scripts/prune-turbopack-cache.ts",
1316
"dev:capped": "NODE_OPTIONS='--max-old-space-size=4096' next dev --port 3000",
17+
"dev:cache:prune": "bun run ../../scripts/prune-turbopack-cache.ts --force",
1418
"dev:clean": "rm -rf .next/dev/cache",
1519
"dev:webpack": "next dev --webpack",
1620
"load:workflow": "bun run load:workflow:baseline",

0 commit comments

Comments
 (0)