fix(build): tree-shake React reconciler dev build to prevent PerformanceMeasure leak#4462
Conversation
…nceMeasure leak The ink 6→7 upgrade (v0.15.11) pulled in react-reconciler 0.33, whose development build calls performance.measure() on every component render. Since NODE_ENV was never set to "production" in the esbuild define map, the bundle shipped both dev and prod builds and selected dev at runtime, causing an unbounded measureEntryBuffer leak (~45% of heap after moderate use, confirmed via heap snapshots). Set process.env.NODE_ENV to "production" at build time so esbuild statically resolves the conditional require and tree-shakes the entire 15k-line dev build. Bundle shrinks by ~700 KB / 15,800 lines.
📋 Review SummaryThis PR adds 🔍 General Feedback
🎯 Specific Feedback🔴 CriticalNo critical issues identified. 🟡 HighNo high priority issues identified. 🟢 MediumNo medium priority issues identified. 🔵 Low
✅ Highlights
|
|
searched and linked issues. bot, please review again. |
📋 Review SummaryThis PR addresses a significant memory leak (~148 MB / 45% of heap) introduced by the ink 6→7 upgrade by adding a single-line 🔍 General Feedback
🎯 Specific Feedback🔵 Low
Suggested addition: // react-reconciler ≥0.33 (ink 7, PR #3860) gates its dev build behind NODE_ENV✅ Highlights
🧪 Verification CommandsFor the reviewer to quickly validate: npm run bundle && grep -c 'performance\.measure' dist/cli.js # Should be 0
node dist/cli.js --version # Should output 0.16.0Recommendation: ✅ Approve — This is a low-risk, high-impact fix for a well-documented memory leak. The single-line change follows established patterns and the validation evidence is compelling. |
wenshao
left a comment
There was a problem hiding this comment.
No blocking issues found. LGTM! ✅
The change correctly sets process.env.NODE_ENV to "production" in the esbuild define block, which tree-shakes the react-reconciler dev build (~19,737 lines). Verified: performance.measure count in dist/cli.js = 0; build and CLI startup both pass.
— DeepSeek/deepseek-v4-pro via Qwen Code /review
Validation from parallel investigationPosting this to help maintainers prioritize the release that includes this PR. Over the past 10 days I've been tracking the OOM cluster from the user-report side; the heap snapshot finding here lines up perfectly with the symptoms in 12+ reports and explains why upgrading to v0.16.0 (which contains PR #4286 for Reports plausibly explained by this fixFrom the investigation thread (full details in code_agent/docs/investigation-oom-series.md):
Three commenters on #2868 (@supercargotim-rgb, @kstepyra, @gkubon) reported "happens multiple times a day" and "happens constantly" within days of the v0.15.11 release window — frequency spike timing matches. My investigation got close but missed the bullseyeI had identified the regression window (v0.15.10 → v0.15.11) and flagged the ink 7 upgrade (#3860) as a suspect, but I misread the mechanism. From the stack frames (OpenSSL symbols like I should have led with Why the symptom looked like a native leak
Suggestion: ship this fastThis PR fixes a measurable 45% of heap retention. Combined with PR #4286 (already in v0.16.0) it should resolve the bulk of the OOM reports. The reporters on the issues above have been suffering for ~10 days with no good workaround beyond "restart frequently / raise I've posted a correction comment on #4116 walking back my B-TLS hypothesis and pointing readers to this PR. Excellent investigation work — heap snapshots win over stack-frame speculation every time. 👏 |
…enLM#4462) Co-authored-by: Jacob Richman <jacob314@gmail.com>
Summary
process.env.NODE_ENVto"production"in the esbuilddefinemap so the React reconciler's development build is tree-shaken from the production bundle.performance.measure()on every Ink component render. SinceNODE_ENVwas never defined at build time, esbuild bundled both dev and prod builds and selected dev at runtime — leakingPerformanceMeasureobjects into the globalmeasureEntryBufferindefinitely. Heap snapshot analysis shows this single buffer retains ~148 MB (45% of heap) after moderate usage, contributing to OOM crashes at the 4 GB limit.defineaddition inesbuild.config.js. Verify no code relies onprocess.env.NODE_ENV !== "production"at runtime in the bundled output.Validation
@qwen-code/qwen-code@0.15.10(ink 6, react-reconciler 0.31): 0performance.measurecalls — no leak@qwen-code/qwen-code@0.15.11(ink 7, react-reconciler 0.33): 17performance.measurecalls — leak introducedmeasureEntryBufferretains 148,173 KB (45% of 331 MB heap) after 3 parallel Explore agents + bash commandsPerformanceMeasureobjects accumulated, never clearedperformance.measurecalls in bundle; dev build fully eliminatednpm run bundle && grep -c 'performance\.measure' dist/cli.js→ should be 0Scope / Risk
process.env.NODE_ENVat runtime in the bundled output will now see"production"instead ofundefined. This is standard practice for Node.js CLI tools and all affected packages (react, react-reconciler, react-dom, react-jsx-runtime) are designed for this.performance.measureleak also affects running from source (node packages/cli/dist/index.js) since tests/dev use the unbundlednode_moduleswhich still do the runtimeNODE_ENVcheck. A separate fix (e.g., settingNODE_ENV=productionin the sandbox launcher) could address that.Testing Matrix
Testing matrix notes:
--version+--helpon macOS arm64Linked Issues / Bugs
Partially addresses #4185 — this fix eliminates one major contributor (~45% of heap) to the OOM crashes in long sessions. The remaining memory pressure from conversation history growth (#4184, #4185) is a separate concern.
The ink 6→7 upgrade landed in v0.15.11 (May 13) via PR #3860. A wave of OOM reports appeared immediately after:
🤖 Generated with Claude Code