必须用 node --inspect 启动,不能靠 VSCode launch.json;正确做法是终端执行 node --inspect=9229 app.js 再以 attach 模式连接,配合 Chrome DevTools Memory 面板拍三次快照对比,重点排查 Detached 对象和 EventEmitter 监听器泄漏。

必须用 node --inspect 启动,不能靠 VSCode 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 场景下常见于:
-
setInterval回调用箭头函数捕获了外部大对象,但忘了clearInterval -
server.on('request', handler)在循环中反复绑定,没调用server.removeAllListeners() - 模块顶层定义的
Map缓存,key 是请求对象或临时Buffer,没做弱引用或 TTL 清理 - 未解绑的
webview.onDidReceiveMessage或未调用dispose()的插件资源
展开 Detached 对象的 Retainers 面板,若引用链里出现 cache、handlers、pendingRequests 或大 Buffer,基本就是泄漏根因。
别只盯着 Closure,也要查 EventEmitter 监听器堆积
单个 EventEmitter 实例注册监听器超过 10 个时,Node.js 会抛 MaxListenersExceededWarning。这不是警告,是泄漏信号。
配合 --trace-warnings 启动:node --trace-warnings app.js,再在疑似模块里插入:
const util = require('util');
console.log(util.inspect(emitter, { showHidden: true, depth: 2 }));
检查输出中的 _events 和 _eventsCount 字段:
- 若
_eventsCount持续增长,且_events里存在大量重复回调引用,就是监听器泄漏确证 - 尤其注意
on/once混用、未配对off、监听器内又注册新监听器等嵌套场景
真正难定位的泄漏,往往藏在看似无害的 setTimeout 回调闭包里,或者被 process.nextTick 拖住的引用链末端——这些地方,快照里 Retained Size 很高,但引用路径极深,得一层层点开看。


















