@exxatdesignux/ui 0.5.9 → 0.5.10

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.10
4
+
5
+ ### Patch Changes
6
+
7
+ - **Turbopack memory cap + cache-bust scripts.** The Next 16.1+ Turbopack file-system cache (default-on) was writing 800+ `.meta` files per process and growing `.next/` to **3+ GB on disk**, which was then mmap'd back into RSS — adding several GB of resident memory per `next-server` on top of what 0.5.9 already capped. Three additions to the scaffolded `template/`:
8
+ - **`turbopack: { memoryLimit: 4 * 1024 * 1024 * 1024 }`** in `next.config.mjs` (top-level, **not** under `experimental` — that key was removed in Next 16). Hard 4 GiB cap on the Turbopack worker; prevents the unbounded growth observed when the FS cache and module graph accumulate over a long dev session.
9
+ - **`pnpm clean`** (`rm -rf .next`) and **`pnpm clean:cache`** (`rm -rf .next/dev/cache .next/dev/trace .next/diagnostics`) — one-command cache bust when `.next` crosses ~2 GB. The FS cache is kept enabled (cold start is ~15s without it) but is now explicitly disposable.
10
+ - **`pnpm dev:fresh`** (`pnpm clean:cache && pnpm dev`) — bust + restart in a single command. Use after a major dependency upgrade or whenever HMR starts skipping updates.
11
+ - **`perf-memory-pattern.md` gains two new sections:**
12
+ - **§3 Turbopack file-system cache** explains the trade-off, when to bust, and why disabling the cache outright is the wrong call.
13
+ - **§4 Don't run two dev servers at the same time** — the #1 cause of memory exhaustion in practice. Two checkouts of the same monorepo (`DS_Workspace/` + `Exxat-DS-Workspace/`), or a customer app + `apps/web` running simultaneously, each carry their own ~2 GB of caches and don't share anything. Includes a `ps` / `lsof` diagnostic table for identifying which checkout owns which `next-server` lineage.
14
+ - **`perf-memory-pattern.md` §6 diagnose table** now leads with the boring checks (`du -sh .next`, `ps aux | grep next-server | wc -l`, `lsof -p <pid> | wc -l`) before suggesting a heap profile — most "high RSS" reports are dual-server or stale-cache, not a real leak.
15
+ - **`consumer-upgrade-checklist.md` §4** gains a `≥ 0.5.10` block listing the three new knobs and the one-time `pnpm clean && pnpm dev` step that customer apps need after upgrading (pre-0.5.10 cache files were written without the memory cap).
16
+
3
17
  ## 0.5.9
4
18
 
5
19
  ### Patch Changes
@@ -30,7 +30,7 @@ Use it when you need to know **what files exist**, **how shims re-export** `@exx
30
30
  - Keep **`@exxatdesignux/ui`** on the same semver your team tested; prefer explicit **`^x.y.z`** or pinned **`x.y.z`**.
31
31
  - Match **`engines.node`** in your app to the value declared in **`node_modules/@exxatdesignux/ui/package.json`** (see CHANGELOG if it changed).
32
32
  - **≥ 0.5.3:** Remove **`vaul`** from your app `package.json` and delete any `components/ui/drawer.tsx` shim — side panels use **`Sheet`** only (**`.cursor/rules/exxat-no-vaul.mdc`**).
33
- - **≥ 0.5.9:** Bump to **Node 24** (LTS-track) and apply the dev-memory tuning. The template ships these by default; existing apps need a one-time copy:
33
+ - **≥ 0.5.9:** Bump to **Node 24** (LTS-track) and apply the dev-memory tuning. The template ships these by default; existing apps need a one-time copy. *(0.5.10 adds the Turbopack cap below; apply both together.)*
34
34
 
35
35
  | What | Where | Effect |
36
36
  |------|-------|--------|
@@ -44,7 +44,17 @@ Use it when you need to know **what files exist**, **how shims re-export** `@exx
44
44
  | `env: { NODE_OPTIONS, NEXT_TELEMETRY_DISABLED }` + `max_memory_restart: "7G"` | `ecosystem.config.cjs` (if using pm2) | Daemon recycles before macOS swaps |
45
45
  | New `pnpm dev:profile` script | `package.json` | `--heap-prof` + `--cpu-prof` snapshots dropped into `.next/diagnostics/` |
46
46
 
47
- Full rationale + diagnostics in **`docs/exxat-ds/perf-memory-pattern.md`** (after `sync-extras`) or **[`apps/web/docs/perf-memory-pattern.md`](https://github.com/ExxatDesign/Exxat-DS-Workspace/blob/main/apps/web/docs/perf-memory-pattern.md)**.
47
+ - **≥ 0.5.10:** Cap Turbopack and add cache-bust scripts. The dev FS cache is enabled by default in Next 16.1 — without these knobs it grows to 2–3 GB on disk and mmaps the same back into RSS:
48
+
49
+ | What | Where | Effect |
50
+ |------|-------|--------|
51
+ | `turbopack: { memoryLimit: 4 * 1024 * 1024 * 1024 }` | `next.config.mjs` (top-level, **not** under `experimental`) | Hard 4 GiB cap on the Turbopack worker — prevents the unbounded cache → RSS growth |
52
+ | New `pnpm clean` (`rm -rf .next`) and `pnpm clean:cache` (`rm -rf .next/dev/cache .next/dev/trace .next/diagnostics`) | `package.json` `scripts` | One-command cache bust when `.next` > 2 GB |
53
+ | New `pnpm dev:fresh` (`pnpm clean:cache && pnpm dev`) | `package.json` `scripts` | Bust + restart in one shot |
54
+
55
+ **Run `pnpm clean && pnpm dev` once after the upgrade.** Pre-0.5.10 cache files were written without the memory cap and may carry stale mmap layouts.
56
+
57
+ Full rationale + diagnostics in **`docs/exxat-ds/perf-memory-pattern.md`** (after `sync-extras`) or **[`apps/web/docs/perf-memory-pattern.md`](https://github.com/ExxatDesign/Exxat-DS-Workspace/blob/main/apps/web/docs/perf-memory-pattern.md)** — especially §3 (Turbopack FS cache) and §4 (don't run two dev servers).
48
58
 
49
59
  ## 5. Consumer UI audit (after sync-extras)
50
60
 
@@ -9,15 +9,16 @@ A fresh `next dev` against this app stabilizes around **~1.4 GB RSS** with
9
9
  the settings below. Without them, the same app drifts to **3–6 GB per
10
10
  process** and pm2 will eventually swap or OOM.
11
11
 
12
- ## 1. The five knobs
12
+ ## 1. The six knobs
13
13
 
14
14
  | # | Knob | Why it matters | Where it lives |
15
15
  |---|------|----------------|----------------|
16
16
  | 1 | `NODE_OPTIONS="--max-old-space-size=6144 --max-semi-space-size=64"` | Caps V8 old-space at 6 GB (default on macOS is ~94% of system RAM = unbounded for practical purposes). With a ceiling, V8 GC pressure kicks in earlier and steady-state heap is lower. `--max-semi-space-size=64` widens the young generation so short-lived render allocations don't promote to old-space. | `package.json` `dev*` scripts + `ecosystem.config.cjs` `env` |
17
- | 2 | `experimental.preloadEntriesOnStart: false` | Next compiles routes on first visit instead of pre-warming every entry on dev start. Dev TTFB drops from ~15s ~2s; steady-state heap is ~30% lower. | `next.config.mjs` |
18
- | 3 | `experimental.optimizePackageImports: [...]` | Re-export barrels (`lucide-react`, `@tabler/icons-react`, `motion`, `@dnd-kit/*`, `recharts`) get tree-shaken to leaf imports. Cuts the dev server's parsed-module count by ~40%. | `next.config.mjs` |
19
- | 4 | `experimental.webpackMemoryOptimizations: true` | Drops large in-memory webpack caches at the cost of slightly slower rebuilds. **Only the `pnpm dev:webpack` fallback uses webpack** Turbopack ignores this flag. Keep it on for the rare cases where the webpack path is needed. | `next.config.mjs` |
20
- | 5 | `target: ES2022` (tsconfig) | The TS compiler emits less polyfill scaffolding for `async/await`, optional chaining, nullish coalescing, etc. tsserver in-memory AST shrinks proportionally. Safe with React 19 + Next 16 + Node 24. | `tsconfig.json` |
17
+ | 2 | `turbopack.memoryLimit: 4 GiB` | Hard cap on the Turbopack worker process. Without this, Turbopack's module graph + mmap'd FS cache files grow unbounded — we observed 3.2 GB on disk and 5+ GB RSS per process with no cap. 4 GiB is generous for apps with < ~1000 routes. | `next.config.mjs` `turbopack` |
18
+ | 3 | `experimental.preloadEntriesOnStart: false` | Next compiles routes on first visit instead of pre-warming every entry on dev start. Dev TTFB drops from ~15s → ~2s; steady-state heap is ~30% lower. | `next.config.mjs` |
19
+ | 4 | `experimental.optimizePackageImports: [...]` | Re-export barrels (`lucide-react`, `@tabler/icons-react`, `motion`, `@dnd-kit/*`, `recharts`) get tree-shaken to leaf imports. Cuts the dev server's parsed-module count by ~40%. | `next.config.mjs` |
20
+ | 5 | `experimental.webpackMemoryOptimizations: true` | Drops large in-memory webpack caches at the cost of slightly slower rebuilds. **Only the `pnpm dev:webpack` fallback uses webpack** Turbopack ignores this flag. Keep it on for the rare cases where the webpack path is needed. | `next.config.mjs` |
21
+ | 6 | `target: ES2022` (tsconfig) | The TS compiler emits less polyfill scaffolding for `async/await`, optional chaining, nullish coalescing, etc. tsserver in-memory AST shrinks proportionally. Safe with React 19 + Next 16 + Node 24. | `tsconfig.json` |
21
22
 
22
23
  ## 2. NODE_OPTIONS propagation
23
24
 
@@ -33,7 +34,61 @@ processes inherit and honour it.
33
34
  - **VS Code / Cursor terminals:** these inherit the parent shell env, so the
34
35
  `package.json` script is enough.
35
36
 
36
- ## 3. Two `next-server` processes is normal
37
+ ## 3. Turbopack file-system cache (Next ≥ 16.1)
38
+
39
+ Since Next 16.1, **`experimental.turbopackFileSystemCacheForDev` defaults to `true`** — Turbopack writes compilation artifacts to `.next/dev/cache/turbopack/<hash>/*.meta` and mmaps them across dev sessions. This is what makes the second `next dev` start in ~1 s instead of ~15 s.
40
+
41
+ **The trade-off** is that the cache grows linearly with the number of unique routes / modules you've touched. After a few weeks of feature work it's normal to see:
42
+
43
+ ```bash
44
+ du -sh .next
45
+ # 3.2G .next
46
+ ```
47
+
48
+ Each `.meta` file is mmap'd by the running `next-server`, so the cache size is roughly the floor of the dev process's RSS until the OS evicts pages.
49
+
50
+ **Do not disable** the FS cache (`turbopackFileSystemCacheForDev: false`) — cold-start dev becomes painful (~15–30 s every restart on this app). Instead, **bust the cache** when it grows past 1–2 GB:
51
+
52
+ ```bash
53
+ pnpm clean:cache # removes .next/dev/cache and .next/dev/trace
54
+ pnpm dev:fresh # bust + restart in one command
55
+ ```
56
+
57
+ The `dev:fresh` script is the right move whenever:
58
+
59
+ - A pnpm install changed `@exxatdesignux/ui` or any framework dep.
60
+ - `.next` is > 2 GB and dev memory is climbing.
61
+ - HMR starts skipping updates or compilation gets stuck on a stale module.
62
+
63
+ For a full nuke (build artifacts too):
64
+
65
+ ```bash
66
+ pnpm clean
67
+ ```
68
+
69
+ `turbopack.memoryLimit` (knob #2) prevents the cache from blowing past 4 GiB of RAM even when the on-disk cache is large.
70
+
71
+ ## 4. Don't run two dev servers at the same time
72
+
73
+ **This is the #1 cause of memory exhaustion in practice.** A single `next-server` (parent + render worker) stabilizes at ~2 GB total RSS with the knobs above. Two parallel servers stabilize at ~4 GB, three at ~6 GB, and so on — they don't share any caches.
74
+
75
+ If you see two `next-server` lineages in `ps`, check:
76
+
77
+ ```bash
78
+ ps aux | grep next-server | grep -v grep
79
+ lsof -p <pid> | grep '\.next/dev/cache' | head -5 # which checkout owns it
80
+ ```
81
+
82
+ Common dual-server scenarios:
83
+
84
+ | Scenario | Symptom | Fix |
85
+ |----------|---------|-----|
86
+ | Two checkouts of the same monorepo (e.g. `DS_Workspace/` and `Exxat-DS-Workspace/`) both running `pnpm dev:web` | Two `next-server` parents, both in `apps/web/.next/...` paths but on different absolute roots | Quit one. Pin to a single checkout per machine. |
87
+ | A customer app (e.g. `test-9`) + the monorepo `apps/web` running at the same time | Different cache hashes (`ee6e79b1/`) under different roots | Stop whichever you're not actively touching: `pm2 stop exxat-ds` or `Ctrl+C` |
88
+ | A stale pm2 daemon from a prior `nvm use 22` session left running after upgrading to Node 24 | One Node-22 + one Node-24 dev server | `pm2 delete exxat-ds` then `pnpm dev:daemon` to re-launch under Node 24 |
89
+ | `next build` running in another tab while `next dev` is up | Three or four `next-server` for the duration of the build | Wait for the build, or kill the dev server until the build is done |
90
+
91
+ ## 5. Two `next-server` processes per app is normal
37
92
 
38
93
  Next 16 splits dev into:
39
94
 
@@ -45,12 +100,17 @@ config in this app the totals stabilize around **~1.4 GB + ~0.6 GB ≈ 2 GB**.
45
100
  If you ever see a third or fourth `next-server`, that's the build worker
46
101
  spawning during a route compile — they exit when the build completes.
47
102
 
48
- ## 4. Diagnose a memory regression
103
+ ## 6. Diagnose a memory regression
49
104
 
50
105
  When dev RSS climbs past 4 GB and stays there:
51
106
 
52
107
  ```bash
53
- # Profile a 60s window and write heap snapshots to .next/diagnostics/
108
+ # First, check the boring stuff
109
+ du -sh .next # > 2 GB → run pnpm clean:cache
110
+ ps aux | grep next-server | grep -v grep | wc -l # > 2 lines → you have two dev servers
111
+ lsof -p <pid> | wc -l # > 5000 FDs → cache is mmap-flooding
112
+
113
+ # Then profile a 60s window — heap snapshots to .next/diagnostics/
54
114
  pnpm dev:profile
55
115
 
56
116
  # Open the latest .heapprofile in Chrome DevTools → Memory → "Load"
@@ -59,15 +119,18 @@ ls -lt .next/diagnostics | head -3
59
119
 
60
120
  Common culprits and their signatures:
61
121
 
62
- | Symptom in the heap snapshot | Likely cause | Fix |
63
- |------------------------------|--------------|-----|
64
- | Many copies of `lucide-react.js` / `recharts.js` retained | Missing entry in `optimizePackageImports` | Add the package to the list (knob 3) |
65
- | Compiled chunks for routes you never visited | `preloadEntriesOnStart` is `true` | Set to `false` (knob 2) |
66
- | Heap grows on every HMR cycle, never shrinks | RSC HMR cache + import.meta.hot leak | `experimental.serverComponentsHmrCache: false` (try only if knob 2 is already on) |
122
+ | Symptom | Likely cause | Fix |
123
+ |---------|--------------|-----|
124
+ | `.next` is > 2 GB on disk; many `.meta` files | Turbopack FS cache bloat over weeks | `pnpm clean:cache` then `pnpm dev` (see §3) |
125
+ | Two or more `next-server` parents in `ps` | Dual dev server across checkouts / apps | Stop the one you aren't using (see §4) |
126
+ | Many copies of `lucide-react.js` / `recharts.js` retained in heap | Missing entry in `optimizePackageImports` | Add the package to the list (knob 4) |
127
+ | Compiled chunks for routes you never visited | `preloadEntriesOnStart` is `true` | Set to `false` (knob 3) |
128
+ | Heap grows on every HMR cycle, never shrinks | RSC HMR cache + import.meta.hot leak | `experimental.serverComponentsHmrCache: false` (try only if knob 3 is already on) |
67
129
  | Single retainer chain holds 100 MB+ | A module-level `Map` / `Set` in app code never gets cleared | Move to request-scoped storage |
68
130
  | tsserver alone is > 1.5 GB | TS strict + large lib check | `skipLibCheck: true` (already on), drop `allowJs` if not needed |
131
+ | Turbopack worker RSS keeps growing past 4 GiB | `turbopack.memoryLimit` not set | Apply knob 2 |
69
132
 
70
- ## 5. Node 24 features we leverage
133
+ ## 7. Node 24 features we leverage
71
134
 
72
135
  Node 24 (LTS-track) is required by `engines.node` in `package.json` and
73
136
  pinned in `.nvmrc`. Specifically:
@@ -93,7 +156,7 @@ pinned in `.nvmrc`. Specifically:
93
156
  - **Smaller initial heap allocations** — V8 13.6 starts with ~50 MB less
94
157
  reserved arena vs V8 12.x. Most visible in fast CI test runs.
95
158
 
96
- ## 6. Anti-patterns
159
+ ## 8. Anti-patterns
97
160
 
98
161
  | Anti-pattern | Why it's wrong |
99
162
  |--------------|----------------|
@@ -104,27 +167,35 @@ pinned in `.nvmrc`. Specifically:
104
167
  | Adding `nodemon` on top of `next dev` | Next has its own watcher; nodemon doubles the file-system event handlers. |
105
168
  | Importing `@exxatdesignux/ui` from the package root for every icon | Defeats `optimizePackageImports`. Always import from the leaf path the DS exposes. |
106
169
  | Running pm2 without `max_memory_restart` | A wedged worker stays wedged. The 7 GB ceiling lets pm2 recycle before the OS swaps. |
170
+ | Disabling `turbopackFileSystemCacheForDev` because "cache is the problem" | Cold starts go from ~1s to ~15–30s every restart. Bust with `pnpm clean:cache` instead. |
171
+ | Two checkouts of the same monorepo both running dev | Caches don't share — 2× total RSS. Pin to one checkout per machine. |
107
172
 
108
- ## 7. Upgrading an existing customer app
173
+ ## 9. Upgrading an existing customer app
109
174
 
110
- If your app was scaffolded before `@exxatdesignux/ui@0.5.9`, copy the diffs
175
+ If your app was scaffolded before `@exxatdesignux/ui@0.5.10`, copy the diffs
111
176
  below from `node_modules/@exxatdesignux/ui/template/`:
112
177
 
113
178
  1. `.nvmrc` — set to `24`.
114
179
  2. `package.json` — `engines.node: ">=24.0.0"` + the `NODE_OPTIONS` /
115
180
  `NEXT_TELEMETRY_DISABLED` prefix on every `dev*` script + the new
116
- `dev:profile` script.
117
- 3. `next.config.mjs` — add the expanded `experimental.optimizePackageImports`
118
- array, `experimental.preloadEntriesOnStart: false`,
119
- `experimental.webpackMemoryOptimizations: true`, and the `onDemandEntries`
120
- block.
181
+ `dev:profile`, `dev:fresh`, `clean`, `clean:cache` scripts.
182
+ 3. `next.config.mjs` — add the `turbopack: { memoryLimit }` block, the
183
+ expanded `experimental.optimizePackageImports` array,
184
+ `experimental.preloadEntriesOnStart: false`,
185
+ `experimental.webpackMemoryOptimizations: true`, and the
186
+ `onDemandEntries` block.
121
187
  4. `tsconfig.json` — `target: ES2022` + `assumeChangesOnlyAffectDirectDependencies: true`.
122
188
  5. `ecosystem.config.cjs` (if used) — add the `env` block with
123
189
  `NODE_OPTIONS` + `NEXT_TELEMETRY_DISABLED` and `max_memory_restart: "7G"`.
124
190
  6. Run `nvm install 24 && nvm use` (or your Node manager equivalent).
191
+ 7. **First run after upgrading:** `pnpm clean && pnpm dev` to drop the
192
+ pre-0.5.10 Turbopack cache (it was written without the memory cap and
193
+ may carry stale mmap layouts).
125
194
 
126
195
  Restart the dev server. You should see steady-state RSS settle in the
127
- 1.5–2 GB range within ~30 s of the first navigation.
196
+ 1.5–2 GB range within ~30 s of the first navigation. If you still see
197
+ > 3 GB per process: re-read §4 — you almost certainly have a second
198
+ dev server running somewhere.
128
199
 
129
200
  ## See also
130
201
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exxatdesignux/ui",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "Exxat shared design system (components, hooks, tokens). Monorepo setup: clone repo then pnpm bootstrap at workspace root — see github.com/ExxatDesign/Exxat-DS-Workspace README.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Exxat Design",
@@ -9,15 +9,16 @@ A fresh `next dev` against this app stabilizes around **~1.4 GB RSS** with
9
9
  the settings below. Without them, the same app drifts to **3–6 GB per
10
10
  process** and pm2 will eventually swap or OOM.
11
11
 
12
- ## 1. The five knobs
12
+ ## 1. The six knobs
13
13
 
14
14
  | # | Knob | Why it matters | Where it lives |
15
15
  |---|------|----------------|----------------|
16
16
  | 1 | `NODE_OPTIONS="--max-old-space-size=6144 --max-semi-space-size=64"` | Caps V8 old-space at 6 GB (default on macOS is ~94% of system RAM = unbounded for practical purposes). With a ceiling, V8 GC pressure kicks in earlier and steady-state heap is lower. `--max-semi-space-size=64` widens the young generation so short-lived render allocations don't promote to old-space. | `package.json` `dev*` scripts + `ecosystem.config.cjs` `env` |
17
- | 2 | `experimental.preloadEntriesOnStart: false` | Next compiles routes on first visit instead of pre-warming every entry on dev start. Dev TTFB drops from ~15s ~2s; steady-state heap is ~30% lower. | `next.config.mjs` |
18
- | 3 | `experimental.optimizePackageImports: [...]` | Re-export barrels (`lucide-react`, `@tabler/icons-react`, `motion`, `@dnd-kit/*`, `recharts`) get tree-shaken to leaf imports. Cuts the dev server's parsed-module count by ~40%. | `next.config.mjs` |
19
- | 4 | `experimental.webpackMemoryOptimizations: true` | Drops large in-memory webpack caches at the cost of slightly slower rebuilds. **Only the `pnpm dev:webpack` fallback uses webpack** Turbopack ignores this flag. Keep it on for the rare cases where the webpack path is needed. | `next.config.mjs` |
20
- | 5 | `target: ES2022` (tsconfig) | The TS compiler emits less polyfill scaffolding for `async/await`, optional chaining, nullish coalescing, etc. tsserver in-memory AST shrinks proportionally. Safe with React 19 + Next 16 + Node 24. | `tsconfig.json` |
17
+ | 2 | `turbopack.memoryLimit: 4 GiB` | Hard cap on the Turbopack worker process. Without this, Turbopack's module graph + mmap'd FS cache files grow unbounded — we observed 3.2 GB on disk and 5+ GB RSS per process with no cap. 4 GiB is generous for apps with < ~1000 routes. | `next.config.mjs` `turbopack` |
18
+ | 3 | `experimental.preloadEntriesOnStart: false` | Next compiles routes on first visit instead of pre-warming every entry on dev start. Dev TTFB drops from ~15s → ~2s; steady-state heap is ~30% lower. | `next.config.mjs` |
19
+ | 4 | `experimental.optimizePackageImports: [...]` | Re-export barrels (`lucide-react`, `@tabler/icons-react`, `motion`, `@dnd-kit/*`, `recharts`) get tree-shaken to leaf imports. Cuts the dev server's parsed-module count by ~40%. | `next.config.mjs` |
20
+ | 5 | `experimental.webpackMemoryOptimizations: true` | Drops large in-memory webpack caches at the cost of slightly slower rebuilds. **Only the `pnpm dev:webpack` fallback uses webpack** Turbopack ignores this flag. Keep it on for the rare cases where the webpack path is needed. | `next.config.mjs` |
21
+ | 6 | `target: ES2022` (tsconfig) | The TS compiler emits less polyfill scaffolding for `async/await`, optional chaining, nullish coalescing, etc. tsserver in-memory AST shrinks proportionally. Safe with React 19 + Next 16 + Node 24. | `tsconfig.json` |
21
22
 
22
23
  ## 2. NODE_OPTIONS propagation
23
24
 
@@ -33,7 +34,61 @@ processes inherit and honour it.
33
34
  - **VS Code / Cursor terminals:** these inherit the parent shell env, so the
34
35
  `package.json` script is enough.
35
36
 
36
- ## 3. Two `next-server` processes is normal
37
+ ## 3. Turbopack file-system cache (Next ≥ 16.1)
38
+
39
+ Since Next 16.1, **`experimental.turbopackFileSystemCacheForDev` defaults to `true`** — Turbopack writes compilation artifacts to `.next/dev/cache/turbopack/<hash>/*.meta` and mmaps them across dev sessions. This is what makes the second `next dev` start in ~1 s instead of ~15 s.
40
+
41
+ **The trade-off** is that the cache grows linearly with the number of unique routes / modules you've touched. After a few weeks of feature work it's normal to see:
42
+
43
+ ```bash
44
+ du -sh .next
45
+ # 3.2G .next
46
+ ```
47
+
48
+ Each `.meta` file is mmap'd by the running `next-server`, so the cache size is roughly the floor of the dev process's RSS until the OS evicts pages.
49
+
50
+ **Do not disable** the FS cache (`turbopackFileSystemCacheForDev: false`) — cold-start dev becomes painful (~15–30 s every restart on this app). Instead, **bust the cache** when it grows past 1–2 GB:
51
+
52
+ ```bash
53
+ pnpm clean:cache # removes .next/dev/cache and .next/dev/trace
54
+ pnpm dev:fresh # bust + restart in one command
55
+ ```
56
+
57
+ The `dev:fresh` script is the right move whenever:
58
+
59
+ - A pnpm install changed `@exxatdesignux/ui` or any framework dep.
60
+ - `.next` is > 2 GB and dev memory is climbing.
61
+ - HMR starts skipping updates or compilation gets stuck on a stale module.
62
+
63
+ For a full nuke (build artifacts too):
64
+
65
+ ```bash
66
+ pnpm clean
67
+ ```
68
+
69
+ `turbopack.memoryLimit` (knob #2) prevents the cache from blowing past 4 GiB of RAM even when the on-disk cache is large.
70
+
71
+ ## 4. Don't run two dev servers at the same time
72
+
73
+ **This is the #1 cause of memory exhaustion in practice.** A single `next-server` (parent + render worker) stabilizes at ~2 GB total RSS with the knobs above. Two parallel servers stabilize at ~4 GB, three at ~6 GB, and so on — they don't share any caches.
74
+
75
+ If you see two `next-server` lineages in `ps`, check:
76
+
77
+ ```bash
78
+ ps aux | grep next-server | grep -v grep
79
+ lsof -p <pid> | grep '\.next/dev/cache' | head -5 # which checkout owns it
80
+ ```
81
+
82
+ Common dual-server scenarios:
83
+
84
+ | Scenario | Symptom | Fix |
85
+ |----------|---------|-----|
86
+ | Two checkouts of the same monorepo (e.g. `DS_Workspace/` and `Exxat-DS-Workspace/`) both running `pnpm dev:web` | Two `next-server` parents, both in `apps/web/.next/...` paths but on different absolute roots | Quit one. Pin to a single checkout per machine. |
87
+ | A customer app (e.g. `test-9`) + the monorepo `apps/web` running at the same time | Different cache hashes (`ee6e79b1/`) under different roots | Stop whichever you're not actively touching: `pm2 stop exxat-ds` or `Ctrl+C` |
88
+ | A stale pm2 daemon from a prior `nvm use 22` session left running after upgrading to Node 24 | One Node-22 + one Node-24 dev server | `pm2 delete exxat-ds` then `pnpm dev:daemon` to re-launch under Node 24 |
89
+ | `next build` running in another tab while `next dev` is up | Three or four `next-server` for the duration of the build | Wait for the build, or kill the dev server until the build is done |
90
+
91
+ ## 5. Two `next-server` processes per app is normal
37
92
 
38
93
  Next 16 splits dev into:
39
94
 
@@ -45,12 +100,17 @@ config in this app the totals stabilize around **~1.4 GB + ~0.6 GB ≈ 2 GB**.
45
100
  If you ever see a third or fourth `next-server`, that's the build worker
46
101
  spawning during a route compile — they exit when the build completes.
47
102
 
48
- ## 4. Diagnose a memory regression
103
+ ## 6. Diagnose a memory regression
49
104
 
50
105
  When dev RSS climbs past 4 GB and stays there:
51
106
 
52
107
  ```bash
53
- # Profile a 60s window and write heap snapshots to .next/diagnostics/
108
+ # First, check the boring stuff
109
+ du -sh .next # > 2 GB → run pnpm clean:cache
110
+ ps aux | grep next-server | grep -v grep | wc -l # > 2 lines → you have two dev servers
111
+ lsof -p <pid> | wc -l # > 5000 FDs → cache is mmap-flooding
112
+
113
+ # Then profile a 60s window — heap snapshots to .next/diagnostics/
54
114
  pnpm dev:profile
55
115
 
56
116
  # Open the latest .heapprofile in Chrome DevTools → Memory → "Load"
@@ -59,15 +119,18 @@ ls -lt .next/diagnostics | head -3
59
119
 
60
120
  Common culprits and their signatures:
61
121
 
62
- | Symptom in the heap snapshot | Likely cause | Fix |
63
- |------------------------------|--------------|-----|
64
- | Many copies of `lucide-react.js` / `recharts.js` retained | Missing entry in `optimizePackageImports` | Add the package to the list (knob 3) |
65
- | Compiled chunks for routes you never visited | `preloadEntriesOnStart` is `true` | Set to `false` (knob 2) |
66
- | Heap grows on every HMR cycle, never shrinks | RSC HMR cache + import.meta.hot leak | `experimental.serverComponentsHmrCache: false` (try only if knob 2 is already on) |
122
+ | Symptom | Likely cause | Fix |
123
+ |---------|--------------|-----|
124
+ | `.next` is > 2 GB on disk; many `.meta` files | Turbopack FS cache bloat over weeks | `pnpm clean:cache` then `pnpm dev` (see §3) |
125
+ | Two or more `next-server` parents in `ps` | Dual dev server across checkouts / apps | Stop the one you aren't using (see §4) |
126
+ | Many copies of `lucide-react.js` / `recharts.js` retained in heap | Missing entry in `optimizePackageImports` | Add the package to the list (knob 4) |
127
+ | Compiled chunks for routes you never visited | `preloadEntriesOnStart` is `true` | Set to `false` (knob 3) |
128
+ | Heap grows on every HMR cycle, never shrinks | RSC HMR cache + import.meta.hot leak | `experimental.serverComponentsHmrCache: false` (try only if knob 3 is already on) |
67
129
  | Single retainer chain holds 100 MB+ | A module-level `Map` / `Set` in app code never gets cleared | Move to request-scoped storage |
68
130
  | tsserver alone is > 1.5 GB | TS strict + large lib check | `skipLibCheck: true` (already on), drop `allowJs` if not needed |
131
+ | Turbopack worker RSS keeps growing past 4 GiB | `turbopack.memoryLimit` not set | Apply knob 2 |
69
132
 
70
- ## 5. Node 24 features we leverage
133
+ ## 7. Node 24 features we leverage
71
134
 
72
135
  Node 24 (LTS-track) is required by `engines.node` in `package.json` and
73
136
  pinned in `.nvmrc`. Specifically:
@@ -93,7 +156,7 @@ pinned in `.nvmrc`. Specifically:
93
156
  - **Smaller initial heap allocations** — V8 13.6 starts with ~50 MB less
94
157
  reserved arena vs V8 12.x. Most visible in fast CI test runs.
95
158
 
96
- ## 6. Anti-patterns
159
+ ## 8. Anti-patterns
97
160
 
98
161
  | Anti-pattern | Why it's wrong |
99
162
  |--------------|----------------|
@@ -104,27 +167,35 @@ pinned in `.nvmrc`. Specifically:
104
167
  | Adding `nodemon` on top of `next dev` | Next has its own watcher; nodemon doubles the file-system event handlers. |
105
168
  | Importing `@exxatdesignux/ui` from the package root for every icon | Defeats `optimizePackageImports`. Always import from the leaf path the DS exposes. |
106
169
  | Running pm2 without `max_memory_restart` | A wedged worker stays wedged. The 7 GB ceiling lets pm2 recycle before the OS swaps. |
170
+ | Disabling `turbopackFileSystemCacheForDev` because "cache is the problem" | Cold starts go from ~1s to ~15–30s every restart. Bust with `pnpm clean:cache` instead. |
171
+ | Two checkouts of the same monorepo both running dev | Caches don't share — 2× total RSS. Pin to one checkout per machine. |
107
172
 
108
- ## 7. Upgrading an existing customer app
173
+ ## 9. Upgrading an existing customer app
109
174
 
110
- If your app was scaffolded before `@exxatdesignux/ui@0.5.9`, copy the diffs
175
+ If your app was scaffolded before `@exxatdesignux/ui@0.5.10`, copy the diffs
111
176
  below from `node_modules/@exxatdesignux/ui/template/`:
112
177
 
113
178
  1. `.nvmrc` — set to `24`.
114
179
  2. `package.json` — `engines.node: ">=24.0.0"` + the `NODE_OPTIONS` /
115
180
  `NEXT_TELEMETRY_DISABLED` prefix on every `dev*` script + the new
116
- `dev:profile` script.
117
- 3. `next.config.mjs` — add the expanded `experimental.optimizePackageImports`
118
- array, `experimental.preloadEntriesOnStart: false`,
119
- `experimental.webpackMemoryOptimizations: true`, and the `onDemandEntries`
120
- block.
181
+ `dev:profile`, `dev:fresh`, `clean`, `clean:cache` scripts.
182
+ 3. `next.config.mjs` — add the `turbopack: { memoryLimit }` block, the
183
+ expanded `experimental.optimizePackageImports` array,
184
+ `experimental.preloadEntriesOnStart: false`,
185
+ `experimental.webpackMemoryOptimizations: true`, and the
186
+ `onDemandEntries` block.
121
187
  4. `tsconfig.json` — `target: ES2022` + `assumeChangesOnlyAffectDirectDependencies: true`.
122
188
  5. `ecosystem.config.cjs` (if used) — add the `env` block with
123
189
  `NODE_OPTIONS` + `NEXT_TELEMETRY_DISABLED` and `max_memory_restart: "7G"`.
124
190
  6. Run `nvm install 24 && nvm use` (or your Node manager equivalent).
191
+ 7. **First run after upgrading:** `pnpm clean && pnpm dev` to drop the
192
+ pre-0.5.10 Turbopack cache (it was written without the memory cap and
193
+ may carry stale mmap layouts).
125
194
 
126
195
  Restart the dev server. You should see steady-state RSS settle in the
127
- 1.5–2 GB range within ~30 s of the first navigation.
196
+ 1.5–2 GB range within ~30 s of the first navigation. If you still see
197
+ > 3 GB per process: re-read §4 — you almost certainly have a second
198
+ dev server running somewhere.
128
199
 
129
200
  ## See also
130
201
 
@@ -153,6 +153,13 @@ const SECURITY_HEADERS = [
153
153
  /** @type {import('next').NextConfig} */
154
154
  const nextConfig = {
155
155
  transpilePackages: ["@exxatdesignux/ui"],
156
+ // Hard cap on the Turbopack worker. Without this, the dev cache + module
157
+ // graph + mmap'd .meta files grow unbounded (we observed 3.2 GB on disk
158
+ // and 5+ GB RSS per process). 4 GiB is generous — Turbopack rarely needs
159
+ // more on apps with < ~1000 routes. See `docs/perf-memory-pattern.md` §6.
160
+ turbopack: {
161
+ memoryLimit: 4 * 1024 * 1024 * 1024,
162
+ },
156
163
  // Dev memory tuning — see `docs/perf-memory-pattern.md` for rationale.
157
164
  experimental: {
158
165
  // Tree-shake heavy barrel re-exports. Every package here was identified by
@@ -13,6 +13,9 @@
13
13
  "dev:3001": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack -p 3001",
14
14
  "dev:3005": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack -p 3005",
15
15
  "dev:profile": "NODE_OPTIONS='--max-old-space-size=6144 --heap-prof --heap-prof-interval=512000 --diagnostic-dir=./.next/diagnostics' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack",
16
+ "dev:fresh": "pnpm clean:cache && pnpm dev",
17
+ "clean": "rm -rf .next",
18
+ "clean:cache": "rm -rf .next/dev/cache .next/dev/trace .next/diagnostics",
16
19
  "dev:daemon": "pm2 start ecosystem.config.cjs",
17
20
  "dev:daemon:stop": "pm2 stop exxat-ds",
18
21
  "dev:daemon:restart": "pm2 restart exxat-ds",