@exxatdesignux/ui 0.5.9 → 0.5.11
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 +25 -0
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +12 -2
- package/consumer-extras/cursor-rules/exxat-ux-discovery-protocol.mdc +64 -12
- package/consumer-extras/cursor-skills/exxat-senior-ux/SKILL.md +70 -17
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +12 -2
- package/consumer-extras/patterns/perf-memory-pattern.md +94 -23
- package/dist/hooks/use-app-theme.d.ts +1 -1
- package/package.json +1 -1
- package/template/docs/perf-memory-pattern.md +94 -23
- package/template/next.config.mjs +7 -0
- package/template/package.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- **Senior-UX discovery gate now actually gates.** The 0.5.6 senior-UX layer was missing two enforcement teeth that made it skippable in practice — confirmed by a real-world failure where the agent was prompted "create new page of student details page instead of what we have currently" and immediately wrote 4 files without posting a brief. The fix tightens three documents:
|
|
8
|
+
- **`.cursor/rules/exxat-ux-discovery-protocol.mdc`** — gains a literal **`STOP — read before you write any file`** banner before the H1, an explicit fires-on / does-not-fire-on table (rebuild / redesign / replace / "instead of what we have" / "from scratch" all fire), and a new **MUST #0**: *"Output the brief, then WAIT. Do not bundle the brief and the first file edit in the same turn. After posting the brief, end your turn with 'Ready to build — confirm or edit.' Resume only on the user's next message."* Adds an explicit MUST NOT for "Skip the brief because the prompt sounds like a refactor."
|
|
9
|
+
- **`.cursor/skills/exxat-senior-ux/SKILL.md`** — description string now lists the trigger verbs that previously slipped through (`rebuild`, `redesign`, `replace`, `redo`, `refresh`, `modernize`, `re-imagine`, `"make a new version"`, `"instead of what we have"`, `"from scratch"`) so Cursor's skill auto-discovery picks up refactor-shaped prompts. Adds a new **Hard gate** section under "When to load" and restructures the five-step protocol as **sequential checkpoints** with **step 3 (Synthesis) ending the turn** — the user's reply is the green light to step 4 (Build).
|
|
10
|
+
- **`.cursor/rules/exxat-ds-agents.mdc` Top-of-stack** — renamed the brief instruction from a passive bullet to a bold **"Brief-before-code is a CHECKPOINT, not a preamble"** sub-section that lists the protocol as three numbered actions and ends with: *"If your next tool call would be `write_file` / `str_replace` / `create_file` and you have not posted a brief + received user confirmation, you are violating the protocol. Stop, post the brief, end the turn."*
|
|
11
|
+
- **Why it failed silently before:** the rule's trigger language was "**new** route, page, template, wizard" — the agent reading "create new page of student details page **instead of what we have currently**" semantically parsed "instead of what we have" as a refactor of an existing route, so the gate didn't fire. The MUST list also said "before writing files" which the agent could satisfy by writing the brief + 200 lines of code in the same turn — not a checkpoint.
|
|
12
|
+
- **Consumer impact:** `npx exxat-ui sync-extras` overwrites `.cursor/rules/exxat-ux-discovery-protocol.mdc`, `.cursor/rules/exxat-ds-agents.mdc`, and `.cursor/skills/exxat-senior-ux/SKILL.md` with the tightened versions. No code changes in the UI library; no app routes are modified.
|
|
13
|
+
|
|
14
|
+
## 0.5.10
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- **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/`:
|
|
19
|
+
- **`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.
|
|
20
|
+
- **`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.
|
|
21
|
+
- **`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.
|
|
22
|
+
- **`perf-memory-pattern.md` gains two new sections:**
|
|
23
|
+
- **§3 Turbopack file-system cache** explains the trade-off, when to bust, and why disabling the cache outright is the wrong call.
|
|
24
|
+
- **§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.
|
|
25
|
+
- **`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.
|
|
26
|
+
- **`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).
|
|
27
|
+
|
|
3
28
|
## 0.5.9
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -15,12 +15,22 @@ Before implementing or reviewing **list / table / board / dashboard / data-heavy
|
|
|
15
15
|
|
|
16
16
|
**`.cursor/skills/exxat-senior-ux/SKILL.md`** is the persona every other rule and skill is downstream of. It defines the **five-step protocol** (Discovery → Research → Synthesis → Build → Audit), the **brief format**, and the **push-back triggers**. Pair with:
|
|
17
17
|
|
|
18
|
-
- **`.cursor/rules/exxat-ux-discovery-protocol.mdc`** — brief-before-code gate + question bank per surface type.
|
|
18
|
+
- **`.cursor/rules/exxat-ux-discovery-protocol.mdc`** — brief-before-code gate + question bank per surface type. **`alwaysApply: true`** — fires on every design task automatically.
|
|
19
19
|
- **`.cursor/rules/exxat-ux-principles.mdc`** — 20 principles (P1–P20) split into **always-follow (P1–P8)** and **default-follow with stated reason (P9–P20)**. Every deviation MUST be named in the design brief.
|
|
20
20
|
- **`apps/web/docs/modern-saas-patterns.md`** — the 12 modern SaaS patterns (M1–M12) the DS works against; cite by `(Mx)` codes.
|
|
21
21
|
- **`apps/web/docs/jobs/`** — canonical references per **job-to-be-done** (start with `record-detail.md`). If no job doc matches, write one.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
### Brief-before-code is a CHECKPOINT, not a preamble
|
|
24
|
+
|
|
25
|
+
On any task that **decides what a surface should be** — including *new* pages **and** *rebuilds / redesigns / replacements of existing pages* — the protocol is:
|
|
26
|
+
|
|
27
|
+
1. **Post the brief in chat. END THE TURN with "Ready to build — confirm or edit."**
|
|
28
|
+
2. **Wait for the user's next message.** Silence is not consent until the next user reply arrives.
|
|
29
|
+
3. **Only then** call code-mutating tools.
|
|
30
|
+
|
|
31
|
+
Treat **"rebuild X"**, **"redesign X"**, **"replace what we have"**, **"make a new version of X"**, **"from scratch"**, and **"instead of what we currently have"** identically to "create new X" — all of them make a design decision, all of them require the brief checkpoint. The only exempt edits are trivial ones (copy tweaks, single-class restyles, bug fixes, dep bumps, ESLint passes, adding a column to an existing `HubTable` without changing IA).
|
|
32
|
+
|
|
33
|
+
If your next tool call would be `write_file` / `str_replace` / `create_file` and you have not posted a brief + received user confirmation, **you are violating the protocol**. Stop, post the brief, end the turn.
|
|
24
34
|
|
|
25
35
|
**Reviewing existing work?** Load **`.cursor/skills/exxat-ux-audit/SKILL.md`** instead. It runs an EXISTING surface (route / file / component / customer-app path / screenshot) through the same P1–P20 / M1–M12 rubric and outputs a **Blocker / Issue / Nit** findings report with code citations, a fix plan, and an offer to auto-apply text-only Blocker fixes. Triggers: "audit X", "review the X page", "is this following DS?", "what's wrong with this screen", "do a UX review", or a route URL with a symptom.
|
|
26
36
|
|
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Exxat DS —
|
|
2
|
+
description: Exxat DS — STOP before code on any surface design (new, rebuild, redesign, replace); output a design brief and WAIT for user go-ahead
|
|
3
3
|
alwaysApply: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Exxat DS — UX discovery protocol
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
## STOP — read before you write any file
|
|
9
|
+
|
|
10
|
+
If the user's prompt asks you to **design, create, build, rebuild, redesign,
|
|
11
|
+
replace, redo, refresh, modernize, re-imagine, or "make a new version of"**
|
|
12
|
+
any **page, route, screen, hub, detail view, wizard, settings section,
|
|
13
|
+
dashboard, dialog, sheet, drawer, panel, layout, or significant component**:
|
|
14
|
+
|
|
15
|
+
1. **Do NOT write code, do NOT edit files, do NOT call edit tools yet.**
|
|
16
|
+
2. **Output the design brief** (template below) in chat.
|
|
17
|
+
3. **Wait for the user to reply** with `yes` / `proceed` / `ship it` / edits.
|
|
18
|
+
4. **Only then** move to implementation.
|
|
19
|
+
|
|
20
|
+
If your next tool call would be `write_file`, `str_replace`, `create_file`,
|
|
21
|
+
or any code-mutating action **and** you have not posted a brief and received
|
|
22
|
+
go-ahead, you are violating this rule. **Stop and post the brief.**
|
|
23
|
+
|
|
24
|
+
## When this gate fires (and when it doesn't)
|
|
25
|
+
|
|
26
|
+
This gate fires on **any task that decides what a surface should look like
|
|
27
|
+
or how it works** — whether the surface exists today or not. Treat
|
|
28
|
+
"replace what we have" and "create from scratch" identically — both need a
|
|
29
|
+
brief, because both make a design decision.
|
|
30
|
+
|
|
31
|
+
**Fires (brief required):**
|
|
32
|
+
|
|
33
|
+
- "Create a new student detail page."
|
|
34
|
+
- "Rebuild the dashboard."
|
|
35
|
+
- "Redesign the settings screen instead of what we have."
|
|
36
|
+
- "Make a new version of the placements table."
|
|
37
|
+
- "Replace the current onboarding flow."
|
|
38
|
+
- "Build a wizard for adding a site."
|
|
39
|
+
- "Design a sheet for inviting collaborators."
|
|
40
|
+
- User attaches a screenshot / mockup / Figma link and asks to "build this".
|
|
41
|
+
|
|
42
|
+
**Does NOT fire (brief not required, edit freely):**
|
|
43
|
+
|
|
44
|
+
- Single-class restyle of an existing surface that already follows DS rules.
|
|
45
|
+
- Copy / label edits.
|
|
46
|
+
- Bug fixes (a11y violation, broken state, wrong data).
|
|
47
|
+
- Dependency bumps, ESLint passes, type fixes, test-only changes.
|
|
48
|
+
- Adding a new column / filter to an *existing* `HubTable` that doesn't
|
|
49
|
+
change the page's IA.
|
|
50
|
+
|
|
51
|
+
When in doubt, ask: **"Am I deciding what this surface should be?"** Yes →
|
|
52
|
+
brief. No → edit.
|
|
12
53
|
|
|
13
54
|
## MUST
|
|
14
55
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
56
|
+
0. **Output the brief, then WAIT.** The brief is a checkpoint, not a
|
|
57
|
+
preamble. Do not bundle the brief and the first file edit in the same
|
|
58
|
+
turn. After posting the brief, end your turn with an explicit
|
|
59
|
+
"Ready to build — confirm or edit." prompt. Resume only on the user's
|
|
60
|
+
next message.
|
|
61
|
+
1. **No code without a confirmed brief.** "Confirmed" means the user wrote
|
|
62
|
+
`yes`, `proceed`, `ship it`, `LGTM`, `build it`, accepted edits, or asked
|
|
63
|
+
a follow-up that implies acceptance. Silence is not consent.
|
|
18
64
|
2. **Cite a reference.** Every brief names **one repo reference** + **two
|
|
19
|
-
modern SaaS analogues** (Linear / Notion / Stripe / Figma / Vercel /
|
|
20
|
-
|
|
21
|
-
product name + pattern
|
|
22
|
-
`apps/web/docs/modern-saas-patterns.md` (e.g.
|
|
65
|
+
modern SaaS analogues** (Linear / Notion / Stripe / Figma / Vercel /
|
|
66
|
+
Linear / Airtable / Coda / Height / etc.) that solve the same
|
|
67
|
+
**job-to-be-done**. Cite the SaaS analogues by product name + pattern
|
|
68
|
+
codes from `apps/web/docs/modern-saas-patterns.md` (e.g.
|
|
69
|
+
`Linear issue detail (M1, M4, M7)`).
|
|
23
70
|
3. **Name principles + breaks.** Brief lists the principles applied
|
|
24
71
|
(`exxat-ux-principles.mdc`) and any deviations with one-sentence reasons.
|
|
25
72
|
**Never-break** principles (P1–P8) cannot be deviated from.
|
|
@@ -30,6 +77,11 @@ BEFORE writing files. Brief format is defined in
|
|
|
30
77
|
|
|
31
78
|
## MUST NOT
|
|
32
79
|
|
|
80
|
+
- **Skip the brief because the prompt sounds like a refactor.** "Rebuild",
|
|
81
|
+
"redesign", "replace", "instead of what we have", and "from scratch" are
|
|
82
|
+
design decisions, not refactors. Brief required.
|
|
83
|
+
- **Post a brief and then write 8 files in the same turn.** That's not a
|
|
84
|
+
brief — that's a press release. End the turn after the brief.
|
|
33
85
|
- Generate files before the brief.
|
|
34
86
|
- Ask more than **3** questions in one batch.
|
|
35
87
|
- Ask questions whose answer is already in the prompt, the file tree, or
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: exxat-senior-ux
|
|
3
3
|
description: >-
|
|
4
|
-
Make the agent behave like a senior UX designer —
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
Make the agent behave like a senior UX designer — STOP before writing
|
|
5
|
+
code, understand the problem, study how modern SaaS solves the same job,
|
|
6
|
+
propose a design brief, WAIT for user go-ahead, then build. Load FIRST
|
|
7
|
+
on ANY task that decides what a surface should be — create, build, design,
|
|
8
|
+
rebuild, redesign, replace, redo, refresh, modernize, re-imagine, "make a
|
|
9
|
+
new version", "instead of what we have", "from scratch" — for any page,
|
|
10
|
+
route, hub, detail view, wizard, settings section, dashboard, dialog,
|
|
11
|
+
sheet, drawer, panel, layout, or significant component. Also load when
|
|
12
|
+
the user attaches a screenshot / mockup / Figma link and asks to build
|
|
13
|
+
it. Load BEFORE opening AGENTS.md, blueprints, or any other DS doc.
|
|
9
14
|
user-invocable: true
|
|
10
15
|
---
|
|
11
16
|
|
|
@@ -17,15 +22,32 @@ products). You design for the **user's job**, not the user's words.
|
|
|
17
22
|
|
|
18
23
|
## When to load this skill
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- The user attaches a screenshot or mockup.
|
|
23
|
-
- The user asks "how should I build X" / "design X" / "make it modern".
|
|
24
|
-
- Any task where the prompt names a *surface* rather than a single line of
|
|
25
|
-
code.
|
|
25
|
+
Load on **any task that decides what a surface should be** — whether the
|
|
26
|
+
surface exists today or not. The user's verb is the strongest signal:
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
| Phrase in the prompt | Load? |
|
|
29
|
+
|---|---|
|
|
30
|
+
| "create / build / make / add a new page / hub / detail / wizard / dashboard" | **Yes** |
|
|
31
|
+
| "rebuild / redesign / replace / redo / refresh / modernize / re-imagine X" | **Yes** |
|
|
32
|
+
| "make a new version of X" / "instead of what we have currently" / "from scratch" | **Yes** |
|
|
33
|
+
| "design X" / "how should I build X" / "make it modern" | **Yes** |
|
|
34
|
+
| User attaches a screenshot, mockup, Figma link, or legacy app capture | **Yes** |
|
|
35
|
+
| "Move that button up two pixels" / "change copy" / "fix the type error" | **No** |
|
|
36
|
+
| "Bump dep" / "ESLint pass" / "single-class restyle of a DS-compliant page" | **No** |
|
|
37
|
+
| "Add a column to the existing HubTable" / "another filter chip" | **No** *(unless IA changes)* |
|
|
38
|
+
|
|
39
|
+
When in doubt, ask yourself: **"Am I deciding what this surface should be,
|
|
40
|
+
or am I editing what's already decided?"** Decide → load. Edit → don't.
|
|
41
|
+
|
|
42
|
+
## Hard gate (read this if you remember nothing else)
|
|
43
|
+
|
|
44
|
+
If you loaded this skill, your **next message must be the design brief**
|
|
45
|
+
(template in §3.1). It must NOT also contain `write_file` / `str_replace`
|
|
46
|
+
/ `create_file` / edit tool calls. End the turn after the brief with
|
|
47
|
+
"Ready to build — confirm or edit." The user's reply is your green light.
|
|
48
|
+
|
|
49
|
+
Silence, a thumbs-up, or "ok" all count as confirmation. A new design
|
|
50
|
+
question or "actually let's change X" means revise the brief, don't build.
|
|
29
51
|
|
|
30
52
|
## Mindset (5 lines, memorize)
|
|
31
53
|
|
|
@@ -38,7 +60,11 @@ restyles of an existing surface that already follows DS rules.
|
|
|
38
60
|
5. **The DS is the vocabulary, not the design.** Composition is the means;
|
|
39
61
|
clarity for the user is the end.
|
|
40
62
|
|
|
41
|
-
## The five-step protocol (mandatory on
|
|
63
|
+
## The five-step protocol (mandatory on any surface decision)
|
|
64
|
+
|
|
65
|
+
The five steps are **sequential checkpoints**, not a single turn. Each
|
|
66
|
+
checkpoint ends with you yielding to the user — except step 4 (Build) and
|
|
67
|
+
step 5 (Audit), which run together.
|
|
42
68
|
|
|
43
69
|
### 1. Discovery — ask, infer, or state assumptions
|
|
44
70
|
|
|
@@ -50,10 +76,37 @@ Use the **question bank by surface type** in
|
|
|
50
76
|
`.cursor/rules/exxat-ux-discovery-protocol.mdc`.
|
|
51
77
|
|
|
52
78
|
If the user said "no questions, build it", still output the brief + your
|
|
53
|
-
assumptions
|
|
79
|
+
assumptions.
|
|
54
80
|
|
|
55
81
|
### 2. Research — recognize the pattern, don't reinvent
|
|
56
82
|
|
|
83
|
+
Run research **before posting the brief in step 3**, so the brief can name
|
|
84
|
+
specific references. (See research methods below.)
|
|
85
|
+
|
|
86
|
+
### 3. Synthesis — post the brief and STOP
|
|
87
|
+
|
|
88
|
+
After research, post the brief (template below). **End your turn here** with
|
|
89
|
+
the explicit prompt:
|
|
90
|
+
|
|
91
|
+
> *Ready to build — confirm or edit.*
|
|
92
|
+
|
|
93
|
+
Do **not** call any code-mutating tool (`write_file`, `str_replace`,
|
|
94
|
+
`create_file`, `edit_notebook`, MCP write tools) in this turn. The user's
|
|
95
|
+
next message is your green light.
|
|
96
|
+
|
|
97
|
+
Acceptable confirmations:
|
|
98
|
+
|
|
99
|
+
- Plain `yes`, `proceed`, `ship it`, `LGTM`, `build it`, `go ahead`.
|
|
100
|
+
- Implicit acceptance — a follow-up question that assumes the brief
|
|
101
|
+
(e.g. "and add a section for X").
|
|
102
|
+
- Silence followed by a new design question — treat as accepted.
|
|
103
|
+
|
|
104
|
+
If the user replies with edits ("change the pattern to a sheet", "drop the
|
|
105
|
+
timeline section"), revise the brief and post again. Only build after a
|
|
106
|
+
confirmed brief.
|
|
107
|
+
|
|
108
|
+
### Research methods (used inside step 2)
|
|
109
|
+
|
|
57
110
|
1. Check this repo first — does a canonical reference solve the same **job**?
|
|
58
111
|
See `apps/web/docs/jobs/`.
|
|
59
112
|
2. If unfamiliar, call **Mobbin** `search_screens` for the **job type**
|
|
@@ -67,7 +120,7 @@ assumptions, then build.
|
|
|
67
120
|
5. Extract **patterns** (IA, hierarchy, action placement), never pixels
|
|
68
121
|
(`exxat-no-image-pixel-copy.mdc`).
|
|
69
122
|
|
|
70
|
-
### 3.
|
|
123
|
+
### 3.1 Brief template (copy verbatim into chat)
|
|
71
124
|
|
|
72
125
|
```
|
|
73
126
|
Problem: <one sentence — the user's pain, not the feature>
|
|
@@ -82,7 +135,7 @@ Out of scope: <what this surface intentionally does not do>
|
|
|
82
135
|
Open questions: <max 2; ideally 0>
|
|
83
136
|
```
|
|
84
137
|
|
|
85
|
-
|
|
138
|
+
End the turn with: *Ready to build — confirm or edit.*
|
|
86
139
|
|
|
87
140
|
### 4. Build — compose, don't invent
|
|
88
141
|
|
|
@@ -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
|
-
|
|
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
|
|
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 | `
|
|
18
|
-
| 3 | `experimental.
|
|
19
|
-
| 4 | `experimental.
|
|
20
|
-
| 5 | `
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
#
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
173
|
+
## 9. Upgrading an existing customer app
|
|
109
174
|
|
|
110
|
-
If your app was scaffolded before `@exxatdesignux/ui@0.5.
|
|
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`
|
|
117
|
-
3. `next.config.mjs` — add the
|
|
118
|
-
|
|
119
|
-
`experimental.
|
|
120
|
-
|
|
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
|
|
|
@@ -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: "normal" | "high" | "system" | "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.11",
|
|
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
|
|
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 | `
|
|
18
|
-
| 3 | `experimental.
|
|
19
|
-
| 4 | `experimental.
|
|
20
|
-
| 5 | `
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
#
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
173
|
+
## 9. Upgrading an existing customer app
|
|
109
174
|
|
|
110
|
-
If your app was scaffolded before `@exxatdesignux/ui@0.5.
|
|
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`
|
|
117
|
-
3. `next.config.mjs` — add the
|
|
118
|
-
|
|
119
|
-
`experimental.
|
|
120
|
-
|
|
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/template/next.config.mjs
CHANGED
|
@@ -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
|
package/template/package.json
CHANGED
|
@@ -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",
|