必须用 node --inspect 启动而非 launch.json 直接运行,以捕获进程启动初期的内存泄漏;正确做法是终端执行 node --inspect=9229 app.js,再用 vscode attach 模式连接,并在 chrome devtools memory 面板进行三次堆快照对比,重点分析 detached 对象和 eventemitter 监听器堆积。

必须用 node --inspect 启动,不能靠 launch.json 直接运行
VSCode 自带的 launch.json 配置启动 Node.js 时,调试器介入太晚——require 阶段创建的闭包、模块缓存、全局定时器等早期泄漏源根本没机会被捕获。真实泄漏往往就发生在进程启动那几毫秒里。
正确做法是:终端手动执行 node --inspect=9229 app.js(端口可换,但后续配置需一致),再让 VSCode 以 attach 模式连接。这样整个进程生命周期完全可控,GC 前后的堆状态才具备可比性。
-
launch.json中type必须设为attach,port与命令行一致 -
localRoot和remoteRoot都填"${workspaceFolder}",否则快照里看不到变量名 - 开发环境建议加
--max-old-space-size=1024,加速 OOM 触发,缩短定位周期
Chrome DevTools Memory 面板才是唯一可靠入口
VSCode 内置调试器不支持完整的堆快照对比功能。必须打开 chrome://inspect → Configure → 添加 localhost:9229 → 刷新后点击 inspect,进入 Memory 面板操作。
关键不是拍一张快照,而是至少拍三次:
- 空载状态(
snapshot-0):刚连上、未触发任何业务逻辑 - 触发可疑行为后(
snapshot-1):比如调用一次接口、打开一个 Webview、执行一段循环 - 等待 30 秒 GC 后(
snapshot-2):给 V8 充分时间回收,再拍一次
右键任一快照 → Compare to previous snapshot,按 Retained Size 降序排列,重点筛 Closure、Buffer、Timeout、EventEmitter 类型。
Detached 不是 DOM 专属,Node.js 里也大量存在
Chrome DevTools 把“长期存活但无引用路径”的对象统一标为 Detached。Node.js 场景下常见于:
使用一条命令部署ProbeChain Rydberg测试网代理节点。自动注册为Agent(NodeType=1),免gas,支持macOS/Linux/Windows。触发词:/r
-
setInterval回调用箭头函数捕获了外部大对象,但忘了clearInterval -
server.on('request', handler)在循环中反复绑定,没调用server.removeAllListeners() - 模块顶层定义的
Map缓存,key是请求对象或临时Buffer,没做弱引用或 TTL 清理 - 未解绑的
webview.onDidReceiveMessage或未调用dispose()的插件资源
展开 Detached 对象的 Retainers 面板,若引用链里出现 cache、handlers、pendingRequests 或大 Buffer,基本就是泄漏根因。
别只盯着 Closure,也要查 EventEmitter 监听器堆积
单个 EventEmitter 实例注册监听器过多却未清理,是 Node.js 内存泄漏最隐蔽也最常见的原因之一。它不会立刻爆内存,但会随请求量线性增长,最终拖垮服务。
典型表现:
- 快照中
EventEmitter实例数量远超预期(比如上千个) - 每个实例的
Retained Size不高,但总和异常大 - 展开其
listeners数组,发现大量重复或已失效的回调函数
排查时注意:once 注册的监听器会被自动清理,但 on 不会;匿名函数无法被 removeListener 精确移除;server.removeAllListeners() 要在明确生命周期结束时调用,不能只依赖进程退出。
真正难的不是拍快照,而是判断哪个 Detached 对象背后藏着没被 dispose() 的资源,或者哪段 on 绑定漏掉了 off。这种泄漏往往在压测几天后才暴露,但根源就在你写第一行 eventEmitter.on 的时候。










