|
| 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`. |
0 commit comments