next-leak 0.1.2 → 0.2.0
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/README.md +49 -13
- package/THIRD-PARTY-NOTICES.md +2678 -0
- package/dist/bootstrap.js +1 -1
- package/dist/{chunk-PF7KYD5N.js → chunk-6XYFBOL2.js} +12 -3
- package/dist/{chunk-2BQZCZ4Z.js → chunk-E5ZKAANQ.js} +90 -3
- package/dist/{chunk-WHC6S57X.js → chunk-HAKKAIHN.js} +148 -156
- package/dist/{chunk-5ZYZW2BL.js → chunk-MYYPTZJW.js} +3 -1
- package/dist/{chunk-C3KH5Z2W.js → chunk-SKAPGI62.js} +2 -1
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli.js +4 -3
- package/dist/confidence.d.ts +9 -2
- package/dist/{html-report-I4PNSLKY.js → html-report-2SOTNXDG.js} +3 -3
- package/dist/index.js +7 -6
- package/dist/{issue-report-VQEUXP2E.js → issue-report-X5JYAVN5.js} +2 -2
- package/dist/launcher.d.ts +9 -0
- package/dist/ritual.d.ts +21 -2
- package/dist/runner.d.ts +33 -2
- package/dist/trend.d.ts +36 -1
- package/package.json +16 -4
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
> Find out whether your Next.js app actually leaks memory — how much, on which route, and whose fault it is.
|
|
9
9
|
|
|
10
|
-
<img src="https://raw.githubusercontent.com/xabierlameiro/next-leak/main/docs/demo.svg" alt="next-leak finding a real Next.js memory leak (
|
|
10
|
+
<img src="https://raw.githubusercontent.com/xabierlameiro/next-leak/main/docs/demo.svg" alt="next-leak finding a real Next.js memory leak (43-second run, idle time compressed)" width="720">
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
$ npx next-leak . --quick
|
|
@@ -46,7 +46,18 @@ that installing it saves memory, but that what it points at is the real cause.
|
|
|
46
46
|
Across ~25 healthy routes on production applications (PPR, MDX, Auth.js,
|
|
47
47
|
Sentry, i18n), it reported **zero false positives**.
|
|
48
48
|
|
|
49
|
-
Your server's memory climbs
|
|
49
|
+
Your self-hosted Next.js server's memory climbs until Node gives up:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Under Docker or Kubernetes you may not even get that: the process is `OOMKilled`,
|
|
56
|
+
the container exits with **code 137**, and the restart wipes the evidence before
|
|
57
|
+
you can look at it. Almost every report of this ends the same way — *"please
|
|
58
|
+
provide heap snapshots taken after forced GC"* — which almost nobody produces
|
|
59
|
+
correctly. `next-leak` runs that controlled measurement for you and answers with
|
|
60
|
+
evidence a maintainer would accept.
|
|
50
61
|
|
|
51
62
|
Three possible answers, all valuable:
|
|
52
63
|
|
|
@@ -68,21 +79,28 @@ npx next-leak .
|
|
|
68
79
|
For each discovered route, in a fresh process, it runs the validated ritual:
|
|
69
80
|
|
|
70
81
|
```
|
|
71
|
-
warm-up → forced GC → baseline snapshot → [load → idle → GC → sample] ×
|
|
82
|
+
warm-up → forced GC → baseline snapshot → [load → idle → GC → sample] ×4 → snapshot
|
|
72
83
|
```
|
|
73
84
|
|
|
74
|
-
The verdict comes from the **shape of the post-GC curve**: retained heap that
|
|
85
|
+
The verdict comes from the **shape of the post-GC curve**: retained heap that
|
|
86
|
+
keeps growing every cycle is a leak; growth that flattens is warm-up. Where the
|
|
87
|
+
heap sits is noise — 40 MB and 400 MB say nothing on their own — so only the
|
|
88
|
+
shape is judged. The one absolute number involved is the gate a cycle's growth
|
|
89
|
+
must clear to count, and it scales with the traffic that cycle served, so
|
|
90
|
+
changing `--requests` changes how long the run takes and not what it decides.
|
|
91
|
+
Every report prints the gate it used.
|
|
75
92
|
|
|
76
93
|
## Options
|
|
77
94
|
|
|
78
95
|
| Flag | Default | What it does |
|
|
79
96
|
|---|---|---|
|
|
80
97
|
| `--routes <list>` | all | Only measure these routes (comma-separated templates or prefixes) |
|
|
81
|
-
| `--cycles <n>` |
|
|
82
|
-
| `--requests <n>` | 5000 | Requests per cycle |
|
|
98
|
+
| `--cycles <n>` | 4 | Load cycles per route (min 3). The first is dropped as warm-up, so the verdict sees `n − 1` deltas — at 3 it sees two |
|
|
99
|
+
| `--requests <n>` | 5000 | Requests per cycle. Raises sensitivity as well as duration: the growth gate scales with it, down to a noise floor around 5000 |
|
|
83
100
|
| `--connections <n>` | 100 | Concurrent connections |
|
|
84
101
|
| `--idle <seconds>` | 30 | **Maximum** wait before each sample; the run continues as soon as the heap settles |
|
|
85
|
-
| `--
|
|
102
|
+
| `--max-old-space <mb>` | 512 | Heap cap of each measured process. Raise it for apps whose legitimate working set is larger, or they die under measurement |
|
|
103
|
+
| `--quick` | off | Fast preset (2000 requests × 4 cycles, 8s idle) — the exact profile the real-app validation ran with. Same cycle count as the default; what it trades away is traffic per cycle, so it sits on the noise floor and is less sensitive to slow leaks. Explicit flags override it |
|
|
86
104
|
| `--diff-all` | off | Diff snapshots for stable routes too |
|
|
87
105
|
| `--output <dir>` | `<app>/.next-leak` | Where runs are written |
|
|
88
106
|
|
|
@@ -127,13 +145,19 @@ separates them, because each one has a different fix:
|
|
|
127
145
|
| Growth that pauses and resumes (stepwise) | `leak` | A healthy route gives back 20-30% of its growth; a stepwise leak gives back nothing |
|
|
128
146
|
| Native/buffer memory with a flat JS heap | `leak (external)` or an explicit RSS note | Heap, `external` and RSS are sampled and judged separately |
|
|
129
147
|
| A leak in your code vs a dependency vs Next itself | `culprit: src/app/x/page.tsx (your code)` — or the package, or framework internals | Retainer chains mapped through the build's source maps |
|
|
130
|
-
| A run whose own evidence is weak | `low confidence` warnings, or the verdict is withdrawn | Every run audits itself: did the load land, did the heap settle, does one cycle carry the average |
|
|
148
|
+
| A run whose own evidence is weak | `low confidence` warnings, or the verdict is withdrawn | Every run audits itself: did the load land, did the heap settle, does one cycle carry the average, did the heap run into its own ceiling |
|
|
131
149
|
|
|
132
150
|
## Reading the verdicts
|
|
133
151
|
|
|
134
|
-
- **`stable`** —
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
- **`stable`** — no growth this run could detect: across the cycles it ran, the
|
|
153
|
+
post-GC curve never cleared the growth gate printed at the foot of the
|
|
154
|
+
report. That is not proof of absence, and the wording matters — the verdict
|
|
155
|
+
is deliberately biased toward missing a leak rather than inventing one (a
|
|
156
|
+
single flat or falling cycle is enough to call a route stable), so a leak
|
|
157
|
+
that oscillates while it climbs can land here. To press harder, raise
|
|
158
|
+
`--cycles` and `--requests`: both make the run more sensitive. If the heap is
|
|
159
|
+
flat but RSS keeps climbing, the report says so explicitly: that is an
|
|
160
|
+
allocator, external-buffer or fragmentation problem, not a JS-heap leak.
|
|
137
161
|
- **`leak`** — the report names the culprit when attribution resolves: your file (`culprit: src/app/x/page.tsx (your code)`), a dependency (package name), or framework internals. An `ISSUE-<route>.md` draft is generated; if the leak is app-owned, the draft tells you **not** to file it upstream.
|
|
138
162
|
- **`inconclusive`** — sustained sub-threshold growth: measure longer. The CLI prints the exact re-run command (`--routes <those> --cycles 6`).
|
|
139
163
|
- **`failed`** — the route errored under load (auth redirects, POST-only endpoints). >1% non-2xx aborts measurement instead of measuring garbage. That's by design.
|
|
@@ -152,8 +176,10 @@ its own evidence, and anything that undermines a verdict is printed next to it:
|
|
|
152
176
|
|
|
153
177
|
What gets checked: whether the heap actually held still before each sample,
|
|
154
178
|
whether the requests you asked for really landed, whether an early-disconnect
|
|
155
|
-
run disconnected anything, whether one cycle dominates the average,
|
|
156
|
-
|
|
179
|
+
run disconnected anything, whether one cycle dominates the average, whether
|
|
180
|
+
the growth barely clears the noise floor, and whether the heap came close
|
|
181
|
+
enough to its own cap that the curve was clipped by the ceiling rather than by
|
|
182
|
+
the app.
|
|
157
183
|
|
|
158
184
|
When the run didn't observe what a `leak` verdict requires — the heap never
|
|
159
185
|
settled, or an abandonment run abandoned nothing — the verdict is **withdrawn**
|
|
@@ -201,6 +227,11 @@ through the build's source maps.
|
|
|
201
227
|
- **Architectures:** verified on **arm64 and x64** (linux/amd64 in Docker) — same app, same parameters, same verdicts.
|
|
202
228
|
- **Attribution** (naming the file) needs a Turbopack build with server sourcemaps — the Next 15+ default. On webpack builds the registry is empty by design and findings degrade to `unattributed` with raw retainer chains; measurement itself does not depend on it. Note that `output: "standalone"` + `--webpack` produced a bundle that could not start at all on `16.3.0-canary.90` (missing `@swc/helpers`), independently of this tool.
|
|
203
229
|
- Empirically validated on Next **15.5.4, 16.0.x, 16.1.5, 16.2.x and 16.3-canary** (incl. Sentry, OpenTelemetry, PPR and i18n apps), against real reproductions from open issues. The contracts it relies on are stable since Next 13–14, but older versions are untested.
|
|
230
|
+
- **Each measured process runs under a 512 MB heap cap** by default, so a leak
|
|
231
|
+
reaches a ceiling in minutes instead of the hours a production container
|
|
232
|
+
takes. An app whose legitimate working set is larger needs
|
|
233
|
+
`--max-old-space`, or every route dies as an OOM that is not the app's
|
|
234
|
+
fault. When a run's heap gets close to the cap, the report says so.
|
|
204
235
|
- Borderline routes can flip between `stable`/`leak` across runs — more cycles resolves this.
|
|
205
236
|
- The measured app runs with its real environment: routes that call external services will call them under load. Scope with `--routes` and moderate `--requests` accordingly.
|
|
206
237
|
|
|
@@ -213,6 +244,11 @@ pnpm pack:smoke # release gate: installs the real tarball and measures the
|
|
|
213
244
|
pnpm test:mutation # Stryker — slow; run before releases, weekly in CI
|
|
214
245
|
```
|
|
215
246
|
|
|
247
|
+
`pnpm build` also regenerates [THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md)
|
|
248
|
+
from the build's metafile and runs `scripts/check-bundle.mjs`, which fails the
|
|
249
|
+
build if browser tooling ever gets back into `dist/`. Commit the regenerated
|
|
250
|
+
notices when a dependency changes.
|
|
251
|
+
|
|
216
252
|
CI runs typecheck, tests, build, `pnpm audit --prod` and the pack smoke on
|
|
217
253
|
Node 22 and 24; mutation testing runs weekly and uploads its report.
|
|
218
254
|
|