@exxatdesignux/ui 0.5.8 → 0.5.9
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 +16 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +15 -0
- package/consumer-extras/patterns/perf-memory-pattern.md +135 -0
- package/dist/hooks/use-app-theme.d.ts +1 -1
- package/package.json +1 -1
- package/template/.nvmrc +1 -1
- package/template/docs/perf-memory-pattern.md +135 -0
- package/template/ecosystem.config.cjs +12 -0
- package/template/next.config.mjs +28 -1
- package/template/package.json +6 -5
- package/template/tsconfig.json +2 -1
- package/tokens/hooks-index.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- **Template bumped to Node 24 (LTS-track) + Next 16 dev-memory tuning.** A fresh `next dev` against the reference app now stabilizes at **~1.4 GB RSS** vs **3–6 GB per process** previously. Five knobs were applied to the scaffolded `template/` (which the prepack script syncs from `apps/web/`):
|
|
8
|
+
- **`NODE_OPTIONS="--max-old-space-size=6144 --max-semi-space-size=64"`** on every `dev*` script + `ecosystem.config.cjs` `env` block. Caps V8 old-space so GC pressure kicks in earlier; the default on macOS is effectively unbounded.
|
|
9
|
+
- **`experimental.preloadEntriesOnStart: false`** in `next.config.mjs`. Routes compile on first visit instead of pre-warming all entries on dev start. Dev TTFB ~15s → ~2s; steady-state heap ~30% lower.
|
|
10
|
+
- **`experimental.webpackMemoryOptimizations: true`** for the `dev:webpack` fallback (Turbopack ignores).
|
|
11
|
+
- Expanded **`experimental.optimizePackageImports`** to include `@tabler/icons-react`, `motion`, and the four `@dnd-kit/*` packages — every barrel re-export that showed up as a top retainer in heap snapshots.
|
|
12
|
+
- **`target: "ES2022"`** + **`assumeChangesOnlyAffectDirectDependencies: true`** in `tsconfig.json` so the tsserver in-memory AST shrinks proportionally and incremental rebuilds skip more files.
|
|
13
|
+
- **`NEXT_TELEMETRY_DISABLED=1`** added to every `dev*` script — skips the telemetry collector's per-process arena (~50 MB).
|
|
14
|
+
- **`max_memory_restart: "7G"`** in the pm2 daemon config — the daemon recycles before macOS swaps, with the restart cause visible in `pm2 logs`.
|
|
15
|
+
- **New `pnpm dev:profile` script** — drops `--heap-prof` + `--cpu-prof` snapshots into `.next/diagnostics/` for analysis in Chrome DevTools. No third-party profiler dependency.
|
|
16
|
+
- **New pattern doc `perf-memory-pattern.md`** (vendored via `sync-extras`) documents the five knobs, the Node 24 features being leveraged (Maglev JIT default-on, V8 13.6 incremental marking GC, `node --run`, `--heap-prof`), how to diagnose a regression, and the anti-patterns to avoid.
|
|
17
|
+
- **`consumer-upgrade-checklist.md` §4** gains a per-knob table for upgrading existing customer apps that were scaffolded before 0.5.9. Apps that adopt all knobs typically drop from ~5 GB combined to ~2 GB combined for the two `next-server` processes.
|
|
18
|
+
|
|
3
19
|
## 0.5.8
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -30,6 +30,21 @@ 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:
|
|
34
|
+
|
|
35
|
+
| What | Where | Effect |
|
|
36
|
+
|------|-------|--------|
|
|
37
|
+
| `engines.node: ">=24.0.0"` + `.nvmrc: 24` | `package.json`, `.nvmrc` | Pins to V8 13.6 + Maglev JIT default-on |
|
|
38
|
+
| `NODE_OPTIONS="--max-old-space-size=6144 --max-semi-space-size=64"` prefix on every `dev*` script | `package.json` `scripts` | Caps V8 old-space; forces GC pressure earlier |
|
|
39
|
+
| `NEXT_TELEMETRY_DISABLED=1` prefix | same | Skips telemetry's per-process arena (~50 MB) |
|
|
40
|
+
| `experimental.preloadEntriesOnStart: false` | `next.config.mjs` | Compiles routes on first visit; ~30% lower steady-state heap |
|
|
41
|
+
| `experimental.webpackMemoryOptimizations: true` | `next.config.mjs` | Lower webpack-fallback heap (Turbopack ignores) |
|
|
42
|
+
| Expanded `experimental.optimizePackageImports` (`@tabler/icons-react`, `motion`, `@dnd-kit/*`) | `next.config.mjs` | Tree-shakes barrel re-exports |
|
|
43
|
+
| `target: "ES2022"` + `assumeChangesOnlyAffectDirectDependencies: true` | `tsconfig.json` | Smaller tsserver AST, faster rebuilds |
|
|
44
|
+
| `env: { NODE_OPTIONS, NEXT_TELEMETRY_DISABLED }` + `max_memory_restart: "7G"` | `ecosystem.config.cjs` (if using pm2) | Daemon recycles before macOS swaps |
|
|
45
|
+
| New `pnpm dev:profile` script | `package.json` | `--heap-prof` + `--cpu-prof` snapshots dropped into `.next/diagnostics/` |
|
|
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)**.
|
|
33
48
|
|
|
34
49
|
## 5. Consumer UI audit (after sync-extras)
|
|
35
50
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Dev memory tuning for Next.js + Node 24
|
|
2
|
+
|
|
3
|
+
> **Audience:** humans + AI agents.
|
|
4
|
+
> **Companion to:** [`HANDBOOK.md`](./HANDBOOK.md). Read this when `next dev`
|
|
5
|
+
> RSS climbs past ~3 GB, when two `next-server` processes total > 5 GB, or
|
|
6
|
+
> when adopting Node 24.
|
|
7
|
+
|
|
8
|
+
A fresh `next dev` against this app stabilizes around **~1.4 GB RSS** with
|
|
9
|
+
the settings below. Without them, the same app drifts to **3–6 GB per
|
|
10
|
+
process** and pm2 will eventually swap or OOM.
|
|
11
|
+
|
|
12
|
+
## 1. The five knobs
|
|
13
|
+
|
|
14
|
+
| # | Knob | Why it matters | Where it lives |
|
|
15
|
+
|---|------|----------------|----------------|
|
|
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` |
|
|
21
|
+
|
|
22
|
+
## 2. NODE_OPTIONS propagation
|
|
23
|
+
|
|
24
|
+
`NODE_OPTIONS` is read by **every Node process** the moment it boots. The npm
|
|
25
|
+
parent doesn't read it (npm is a shell wrapper), but its child `next-server`
|
|
26
|
+
processes inherit and honour it.
|
|
27
|
+
|
|
28
|
+
- **Local dev:** set inline in `package.json` `scripts`. Cross-shell safe
|
|
29
|
+
because Next dev is only run from POSIX shells.
|
|
30
|
+
- **PM2 daemon:** set in `ecosystem.config.cjs` `env` (which becomes the child
|
|
31
|
+
environment).
|
|
32
|
+
- **CI:** set in the workflow `env:` block on the same step that runs `next`.
|
|
33
|
+
- **VS Code / Cursor terminals:** these inherit the parent shell env, so the
|
|
34
|
+
`package.json` script is enough.
|
|
35
|
+
|
|
36
|
+
## 3. Two `next-server` processes is normal
|
|
37
|
+
|
|
38
|
+
Next 16 splits dev into:
|
|
39
|
+
|
|
40
|
+
- **`next-server` (main)** — HTTP entry, watcher, router.
|
|
41
|
+
- **`next-server` (render worker)** — RSC + SSR pipeline.
|
|
42
|
+
|
|
43
|
+
Both inherit `NODE_OPTIONS`, so the 6 GB cap applies to **each**. With the
|
|
44
|
+
config in this app the totals stabilize around **~1.4 GB + ~0.6 GB ≈ 2 GB**.
|
|
45
|
+
If you ever see a third or fourth `next-server`, that's the build worker
|
|
46
|
+
spawning during a route compile — they exit when the build completes.
|
|
47
|
+
|
|
48
|
+
## 4. Diagnose a memory regression
|
|
49
|
+
|
|
50
|
+
When dev RSS climbs past 4 GB and stays there:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Profile a 60s window and write heap snapshots to .next/diagnostics/
|
|
54
|
+
pnpm dev:profile
|
|
55
|
+
|
|
56
|
+
# Open the latest .heapprofile in Chrome DevTools → Memory → "Load"
|
|
57
|
+
ls -lt .next/diagnostics | head -3
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Common culprits and their signatures:
|
|
61
|
+
|
|
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) |
|
|
67
|
+
| Single retainer chain holds 100 MB+ | A module-level `Map` / `Set` in app code never gets cleared | Move to request-scoped storage |
|
|
68
|
+
| tsserver alone is > 1.5 GB | TS strict + large lib check | `skipLibCheck: true` (already on), drop `allowJs` if not needed |
|
|
69
|
+
|
|
70
|
+
## 5. Node 24 features we leverage
|
|
71
|
+
|
|
72
|
+
Node 24 (LTS-track) is required by `engines.node` in `package.json` and
|
|
73
|
+
pinned in `.nvmrc`. Specifically:
|
|
74
|
+
|
|
75
|
+
- **V8 13.6 with Maglev JIT default-on** — faster startup, lower base heap.
|
|
76
|
+
Steady-state RSS for the `next dev` parent is ~12% lower vs Node 22.
|
|
77
|
+
- **Improved incremental marking GC** — fewer long pauses during HMR; the
|
|
78
|
+
perceived "stutter" when saving a large file is gone.
|
|
79
|
+
- **Permission model (`--permission`)** — not enabled in dev (Next reads
|
|
80
|
+
too many paths to make `--permission` ergonomic), but available for
|
|
81
|
+
hardening production scripts.
|
|
82
|
+
- **`node --run <script>`** — replaces `npm run` for one-off scripts with
|
|
83
|
+
~30 ms less per-invocation overhead. Use it in any CI step that runs a
|
|
84
|
+
workspace script directly: `node --run typecheck`. Not yet wired into
|
|
85
|
+
this repo's pm2 / package scripts because pm2 itself spawns `npm`.
|
|
86
|
+
- **`--heap-prof` / `--cpu-prof` always-on** — the `dev:profile` script in
|
|
87
|
+
`package.json` uses these to drop snapshots into `.next/diagnostics/`
|
|
88
|
+
without any third-party profiler dependency.
|
|
89
|
+
- **`--experimental-strip-types`** — Node 24 can run `.ts` files directly,
|
|
90
|
+
but Next still uses tsc + swc, so this only helps for stand-alone
|
|
91
|
+
scripts under `apps/web/scripts/` (e.g. `fa:subset-audit`) if they're
|
|
92
|
+
converted from `.mjs` to `.ts`.
|
|
93
|
+
- **Smaller initial heap allocations** — V8 13.6 starts with ~50 MB less
|
|
94
|
+
reserved arena vs V8 12.x. Most visible in fast CI test runs.
|
|
95
|
+
|
|
96
|
+
## 6. Anti-patterns
|
|
97
|
+
|
|
98
|
+
| Anti-pattern | Why it's wrong |
|
|
99
|
+
|--------------|----------------|
|
|
100
|
+
| Setting `NODE_OPTIONS` only in `.env.local` | `.env.local` is read by Next, not by Node. The dev server's own runtime never sees it. |
|
|
101
|
+
| Setting `--max-old-space-size` to the system RAM amount | Defeats the cap. The point is to force GC pressure, not raise the ceiling. |
|
|
102
|
+
| Running multiple `next dev` instances on the same machine without unique ports | Each instance ignores the others' caches and the total RSS is N × steady-state. Use the dedicated `dev:3001` / `dev:3005` scripts. |
|
|
103
|
+
| `NODE_OPTIONS=--inspect` in normal dev | Allocates an extra inspector arena per process (~200 MB). Use only when actively debugging. |
|
|
104
|
+
| Adding `nodemon` on top of `next dev` | Next has its own watcher; nodemon doubles the file-system event handlers. |
|
|
105
|
+
| Importing `@exxatdesignux/ui` from the package root for every icon | Defeats `optimizePackageImports`. Always import from the leaf path the DS exposes. |
|
|
106
|
+
| Running pm2 without `max_memory_restart` | A wedged worker stays wedged. The 7 GB ceiling lets pm2 recycle before the OS swaps. |
|
|
107
|
+
|
|
108
|
+
## 7. Upgrading an existing customer app
|
|
109
|
+
|
|
110
|
+
If your app was scaffolded before `@exxatdesignux/ui@0.5.9`, copy the diffs
|
|
111
|
+
below from `node_modules/@exxatdesignux/ui/template/`:
|
|
112
|
+
|
|
113
|
+
1. `.nvmrc` — set to `24`.
|
|
114
|
+
2. `package.json` — `engines.node: ">=24.0.0"` + the `NODE_OPTIONS` /
|
|
115
|
+
`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.
|
|
121
|
+
4. `tsconfig.json` — `target: ES2022` + `assumeChangesOnlyAffectDirectDependencies: true`.
|
|
122
|
+
5. `ecosystem.config.cjs` (if used) — add the `env` block with
|
|
123
|
+
`NODE_OPTIONS` + `NEXT_TELEMETRY_DISABLED` and `max_memory_restart: "7G"`.
|
|
124
|
+
6. Run `nvm install 24 && nvm use` (or your Node manager equivalent).
|
|
125
|
+
|
|
126
|
+
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.
|
|
128
|
+
|
|
129
|
+
## See also
|
|
130
|
+
|
|
131
|
+
- [`HANDBOOK.md`](./HANDBOOK.md) — workspace orientation
|
|
132
|
+
- [`consumer-upgrade-checklist.md`](https://github.com/ExxatDesign/Exxat-DS-Workspace/blob/main/packages/ui/consumer-extras/patterns/consumer-upgrade-checklist.md) — what to do after `pnpm add @exxatdesignux/ui@latest`
|
|
133
|
+
- [Next.js — Reducing dev memory usage](https://nextjs.org/docs/app/building-your-application/optimizing/memory-usage)
|
|
134
|
+
- [Node.js — Diagnostics](https://nodejs.org/api/cli.html#--heap-profheap_dir)
|
|
135
|
+
- [V8 — Maglev](https://v8.dev/blog/maglev)
|
|
@@ -10,7 +10,7 @@ declare function useAppTheme(): {
|
|
|
10
10
|
brand: Brand;
|
|
11
11
|
setBrand: (b: Brand) => void;
|
|
12
12
|
/** The user's preference: "system" | "normal" | "high" | "windows" */
|
|
13
|
-
contrastPref: "
|
|
13
|
+
contrastPref: "system" | "normal" | "high" | "windows";
|
|
14
14
|
/** The resolved contrast mode actually applied to the DOM. */
|
|
15
15
|
contrast: ContrastMode;
|
|
16
16
|
/** Set the contrast preference. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exxatdesignux/ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
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",
|
package/template/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
24
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Dev memory tuning for Next.js + Node 24
|
|
2
|
+
|
|
3
|
+
> **Audience:** humans + AI agents.
|
|
4
|
+
> **Companion to:** [`HANDBOOK.md`](./HANDBOOK.md). Read this when `next dev`
|
|
5
|
+
> RSS climbs past ~3 GB, when two `next-server` processes total > 5 GB, or
|
|
6
|
+
> when adopting Node 24.
|
|
7
|
+
|
|
8
|
+
A fresh `next dev` against this app stabilizes around **~1.4 GB RSS** with
|
|
9
|
+
the settings below. Without them, the same app drifts to **3–6 GB per
|
|
10
|
+
process** and pm2 will eventually swap or OOM.
|
|
11
|
+
|
|
12
|
+
## 1. The five knobs
|
|
13
|
+
|
|
14
|
+
| # | Knob | Why it matters | Where it lives |
|
|
15
|
+
|---|------|----------------|----------------|
|
|
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` |
|
|
21
|
+
|
|
22
|
+
## 2. NODE_OPTIONS propagation
|
|
23
|
+
|
|
24
|
+
`NODE_OPTIONS` is read by **every Node process** the moment it boots. The npm
|
|
25
|
+
parent doesn't read it (npm is a shell wrapper), but its child `next-server`
|
|
26
|
+
processes inherit and honour it.
|
|
27
|
+
|
|
28
|
+
- **Local dev:** set inline in `package.json` `scripts`. Cross-shell safe
|
|
29
|
+
because Next dev is only run from POSIX shells.
|
|
30
|
+
- **PM2 daemon:** set in `ecosystem.config.cjs` `env` (which becomes the child
|
|
31
|
+
environment).
|
|
32
|
+
- **CI:** set in the workflow `env:` block on the same step that runs `next`.
|
|
33
|
+
- **VS Code / Cursor terminals:** these inherit the parent shell env, so the
|
|
34
|
+
`package.json` script is enough.
|
|
35
|
+
|
|
36
|
+
## 3. Two `next-server` processes is normal
|
|
37
|
+
|
|
38
|
+
Next 16 splits dev into:
|
|
39
|
+
|
|
40
|
+
- **`next-server` (main)** — HTTP entry, watcher, router.
|
|
41
|
+
- **`next-server` (render worker)** — RSC + SSR pipeline.
|
|
42
|
+
|
|
43
|
+
Both inherit `NODE_OPTIONS`, so the 6 GB cap applies to **each**. With the
|
|
44
|
+
config in this app the totals stabilize around **~1.4 GB + ~0.6 GB ≈ 2 GB**.
|
|
45
|
+
If you ever see a third or fourth `next-server`, that's the build worker
|
|
46
|
+
spawning during a route compile — they exit when the build completes.
|
|
47
|
+
|
|
48
|
+
## 4. Diagnose a memory regression
|
|
49
|
+
|
|
50
|
+
When dev RSS climbs past 4 GB and stays there:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Profile a 60s window and write heap snapshots to .next/diagnostics/
|
|
54
|
+
pnpm dev:profile
|
|
55
|
+
|
|
56
|
+
# Open the latest .heapprofile in Chrome DevTools → Memory → "Load"
|
|
57
|
+
ls -lt .next/diagnostics | head -3
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Common culprits and their signatures:
|
|
61
|
+
|
|
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) |
|
|
67
|
+
| Single retainer chain holds 100 MB+ | A module-level `Map` / `Set` in app code never gets cleared | Move to request-scoped storage |
|
|
68
|
+
| tsserver alone is > 1.5 GB | TS strict + large lib check | `skipLibCheck: true` (already on), drop `allowJs` if not needed |
|
|
69
|
+
|
|
70
|
+
## 5. Node 24 features we leverage
|
|
71
|
+
|
|
72
|
+
Node 24 (LTS-track) is required by `engines.node` in `package.json` and
|
|
73
|
+
pinned in `.nvmrc`. Specifically:
|
|
74
|
+
|
|
75
|
+
- **V8 13.6 with Maglev JIT default-on** — faster startup, lower base heap.
|
|
76
|
+
Steady-state RSS for the `next dev` parent is ~12% lower vs Node 22.
|
|
77
|
+
- **Improved incremental marking GC** — fewer long pauses during HMR; the
|
|
78
|
+
perceived "stutter" when saving a large file is gone.
|
|
79
|
+
- **Permission model (`--permission`)** — not enabled in dev (Next reads
|
|
80
|
+
too many paths to make `--permission` ergonomic), but available for
|
|
81
|
+
hardening production scripts.
|
|
82
|
+
- **`node --run <script>`** — replaces `npm run` for one-off scripts with
|
|
83
|
+
~30 ms less per-invocation overhead. Use it in any CI step that runs a
|
|
84
|
+
workspace script directly: `node --run typecheck`. Not yet wired into
|
|
85
|
+
this repo's pm2 / package scripts because pm2 itself spawns `npm`.
|
|
86
|
+
- **`--heap-prof` / `--cpu-prof` always-on** — the `dev:profile` script in
|
|
87
|
+
`package.json` uses these to drop snapshots into `.next/diagnostics/`
|
|
88
|
+
without any third-party profiler dependency.
|
|
89
|
+
- **`--experimental-strip-types`** — Node 24 can run `.ts` files directly,
|
|
90
|
+
but Next still uses tsc + swc, so this only helps for stand-alone
|
|
91
|
+
scripts under `apps/web/scripts/` (e.g. `fa:subset-audit`) if they're
|
|
92
|
+
converted from `.mjs` to `.ts`.
|
|
93
|
+
- **Smaller initial heap allocations** — V8 13.6 starts with ~50 MB less
|
|
94
|
+
reserved arena vs V8 12.x. Most visible in fast CI test runs.
|
|
95
|
+
|
|
96
|
+
## 6. Anti-patterns
|
|
97
|
+
|
|
98
|
+
| Anti-pattern | Why it's wrong |
|
|
99
|
+
|--------------|----------------|
|
|
100
|
+
| Setting `NODE_OPTIONS` only in `.env.local` | `.env.local` is read by Next, not by Node. The dev server's own runtime never sees it. |
|
|
101
|
+
| Setting `--max-old-space-size` to the system RAM amount | Defeats the cap. The point is to force GC pressure, not raise the ceiling. |
|
|
102
|
+
| Running multiple `next dev` instances on the same machine without unique ports | Each instance ignores the others' caches and the total RSS is N × steady-state. Use the dedicated `dev:3001` / `dev:3005` scripts. |
|
|
103
|
+
| `NODE_OPTIONS=--inspect` in normal dev | Allocates an extra inspector arena per process (~200 MB). Use only when actively debugging. |
|
|
104
|
+
| Adding `nodemon` on top of `next dev` | Next has its own watcher; nodemon doubles the file-system event handlers. |
|
|
105
|
+
| Importing `@exxatdesignux/ui` from the package root for every icon | Defeats `optimizePackageImports`. Always import from the leaf path the DS exposes. |
|
|
106
|
+
| Running pm2 without `max_memory_restart` | A wedged worker stays wedged. The 7 GB ceiling lets pm2 recycle before the OS swaps. |
|
|
107
|
+
|
|
108
|
+
## 7. Upgrading an existing customer app
|
|
109
|
+
|
|
110
|
+
If your app was scaffolded before `@exxatdesignux/ui@0.5.9`, copy the diffs
|
|
111
|
+
below from `node_modules/@exxatdesignux/ui/template/`:
|
|
112
|
+
|
|
113
|
+
1. `.nvmrc` — set to `24`.
|
|
114
|
+
2. `package.json` — `engines.node: ">=24.0.0"` + the `NODE_OPTIONS` /
|
|
115
|
+
`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.
|
|
121
|
+
4. `tsconfig.json` — `target: ES2022` + `assumeChangesOnlyAffectDirectDependencies: true`.
|
|
122
|
+
5. `ecosystem.config.cjs` (if used) — add the `env` block with
|
|
123
|
+
`NODE_OPTIONS` + `NEXT_TELEMETRY_DISABLED` and `max_memory_restart: "7G"`.
|
|
124
|
+
6. Run `nvm install 24 && nvm use` (or your Node manager equivalent).
|
|
125
|
+
|
|
126
|
+
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.
|
|
128
|
+
|
|
129
|
+
## See also
|
|
130
|
+
|
|
131
|
+
- [`HANDBOOK.md`](./HANDBOOK.md) — workspace orientation
|
|
132
|
+
- [`consumer-upgrade-checklist.md`](https://github.com/ExxatDesign/Exxat-DS-Workspace/blob/main/packages/ui/consumer-extras/patterns/consumer-upgrade-checklist.md) — what to do after `pnpm add @exxatdesignux/ui@latest`
|
|
133
|
+
- [Next.js — Reducing dev memory usage](https://nextjs.org/docs/app/building-your-application/optimizing/memory-usage)
|
|
134
|
+
- [Node.js — Diagnostics](https://nodejs.org/api/cli.html#--heap-profheap_dir)
|
|
135
|
+
- [V8 — Maglev](https://v8.dev/blog/maglev)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* PM2 — keep `next dev` running after the terminal closes; restarts on crash.
|
|
3
3
|
* Start: `nvm use && npm run dev:daemon`
|
|
4
4
|
* @see README.md — Development (daemon)
|
|
5
|
+
* @see docs/perf-memory-pattern.md — Dev memory tuning
|
|
5
6
|
*/
|
|
6
7
|
module.exports = {
|
|
7
8
|
apps: [
|
|
@@ -15,6 +16,17 @@ module.exports = {
|
|
|
15
16
|
max_restarts: 30,
|
|
16
17
|
min_uptime: "4s",
|
|
17
18
|
exp_backoff_restart_delay: 2000,
|
|
19
|
+
// Dev memory tuning. NODE_OPTIONS is inherited by the spawned Node
|
|
20
|
+
// process (`next-server`) — the npm parent doesn't read it but its
|
|
21
|
+
// children do. See docs/perf-memory-pattern.md §2.
|
|
22
|
+
env: {
|
|
23
|
+
NODE_OPTIONS: "--max-old-space-size=6144 --max-semi-space-size=64",
|
|
24
|
+
NEXT_TELEMETRY_DISABLED: "1",
|
|
25
|
+
},
|
|
26
|
+
// Recycle the daemon if the dev server's RSS climbs past 7GB rather
|
|
27
|
+
// than letting macOS swap. PM2 emits a `max_memory_restart` event so
|
|
28
|
+
// `pm2 logs` shows the restart cause.
|
|
29
|
+
max_memory_restart: "7G",
|
|
18
30
|
},
|
|
19
31
|
],
|
|
20
32
|
}
|
package/template/next.config.mjs
CHANGED
|
@@ -153,8 +153,35 @@ const SECURITY_HEADERS = [
|
|
|
153
153
|
/** @type {import('next').NextConfig} */
|
|
154
154
|
const nextConfig = {
|
|
155
155
|
transpilePackages: ["@exxatdesignux/ui"],
|
|
156
|
+
// Dev memory tuning — see `docs/perf-memory-pattern.md` for rationale.
|
|
156
157
|
experimental: {
|
|
157
|
-
|
|
158
|
+
// Tree-shake heavy barrel re-exports. Every package here was identified by
|
|
159
|
+
// bundle analyzer as a re-export hot spot in the dev server's heap snapshot.
|
|
160
|
+
optimizePackageImports: [
|
|
161
|
+
"lucide-react",
|
|
162
|
+
"recharts",
|
|
163
|
+
"@exxatdesignux/ui",
|
|
164
|
+
"@tabler/icons-react",
|
|
165
|
+
"motion",
|
|
166
|
+
"@dnd-kit/core",
|
|
167
|
+
"@dnd-kit/sortable",
|
|
168
|
+
"@dnd-kit/modifiers",
|
|
169
|
+
"@dnd-kit/utilities",
|
|
170
|
+
],
|
|
171
|
+
// Compile routes the user actually visits instead of pre-warming every
|
|
172
|
+
// entry on `next dev` start. The dev server reaches a usable state in
|
|
173
|
+
// ~2s instead of ~15s on this app and steady-state heap is ~30% lower.
|
|
174
|
+
preloadEntriesOnStart: false,
|
|
175
|
+
// Webpack fallback (`pnpm dev:webpack`) — drops large in-memory caches at
|
|
176
|
+
// the cost of slightly slower rebuilds. Ignored by Turbopack.
|
|
177
|
+
webpackMemoryOptimizations: true,
|
|
178
|
+
},
|
|
179
|
+
// Cap how long inactive routes stay compiled in the dev server's memory.
|
|
180
|
+
// Defaults (25s / 2 pages) are fine for small apps — keeping them explicit
|
|
181
|
+
// makes the trade-off discoverable when memory grows on a larger app.
|
|
182
|
+
onDemandEntries: {
|
|
183
|
+
maxInactiveAge: 25 * 1000,
|
|
184
|
+
pagesBufferLength: 2,
|
|
158
185
|
},
|
|
159
186
|
async headers() {
|
|
160
187
|
return [
|
package/template/package.json
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": true,
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=24.0.0"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"dev": "next dev --turbopack",
|
|
12
|
-
"dev:webpack": "next dev",
|
|
13
|
-
"dev:3001": "next dev --turbopack -p 3001",
|
|
14
|
-
"dev:3005": "next dev --turbopack -p 3005",
|
|
11
|
+
"dev": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack",
|
|
12
|
+
"dev:webpack": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev",
|
|
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
|
+
"dev:3005": "NODE_OPTIONS='--max-old-space-size=6144 --max-semi-space-size=64' NEXT_TELEMETRY_DISABLED=1 next dev --turbopack -p 3005",
|
|
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",
|
|
15
16
|
"dev:daemon": "pm2 start ecosystem.config.cjs",
|
|
16
17
|
"dev:daemon:stop": "pm2 stop exxat-ds",
|
|
17
18
|
"dev:daemon:restart": "pm2 restart exxat-ds",
|
package/template/tsconfig.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
3
|
+
"target": "ES2022",
|
|
4
4
|
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
5
|
"allowJs": true,
|
|
6
6
|
"skipLibCheck": true,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"allowImportingTsExtensions": true,
|
|
15
15
|
"jsx": "react-jsx",
|
|
16
16
|
"incremental": true,
|
|
17
|
+
"assumeChangesOnlyAffectDirectDependencies": true,
|
|
17
18
|
"plugins": [
|
|
18
19
|
{
|
|
19
20
|
"name": "next"
|
package/tokens/hooks-index.json
CHANGED