保留树识别死对象的关键是找到未释放的异步闭包,而非层数;需通过Dominators视图筛选高Retained Size的(closure),在[[Scopes]]中定位捕获的已卸载组件或过期对象,并结合GC Roots路径确认其被全局引用、未取消的异步任务或已卸载组件实例持有。

保留树(Retaining Tree)不是看“有多深”,而是看“谁在拽着它不放”。真正要识别被长生命周期异步闭包捕获的死对象,关键不是数层数,而是顺着引用链找到那个本该结束却仍活着的闭包,并确认它捕获了已失效的上下文。
先理解:闭包捕获的是变量绑定,不是对象本身
JavaScript 中的闭包会持有其词法作用域中所有**被引用的变量**。如果一个异步回调(如 setTimeout、Promise.then、fetch.then)在组件卸载后才执行,而它内部访问了组件实例 this、ref、state 或局部大数组,那么这些对象就会被该闭包长期强引用——即使组件早已销毁,它们也无法被回收。
这种闭包往往构造函数显示为 (closure),在 Dominators 视图中 Retained Size 高,但在 Summary 中可能只看到几个实例,容易被忽略。
在保留树中定位“不该存在的闭包”
- 打开堆快照 → 切换到 Dominators 视图 → 按 Retained Size 降序排列
- 重点筛选构造函数为 (closure) 且 Retained Size 明显高于 Shallow Size(比如 1MB+ vs 几 KB)的条目
- 双击该闭包 → 右侧展开 Retaining Tree → 查看它直接持有的属性(如
[[Scopes]]下的Closure节点) - 在
[[Scopes]]中逐层展开,找那些指向已卸载组件、旧 DOM 节点、或过期缓存对象的引用 —— 这些就是被“捕获的死对象”
结合引用链确认“它为什么还活着”
右键该闭包 → Reveal in Summary → 再右键同一行 → Path to GC Roots → 选 with all references:
- 若路径中出现 window.xxxHandler、globalThis.callbacks 或某个静态 Map 的 entry,则说明它被全局注册,未清理
- 若路径经过 PromiseReactionJob、setTimeout 或 EventListener,且下游是已 detached 的 DOM 节点,基本锁定为异步回调未取消
- 若路径里有 React/Vue 组件名(如
MyModal),但该组件在当前页面已 unmount,而闭包仍持有着它的this或ref.current,这就是典型死对象被捕获
交叉验证:用多快照差分抓增长证据
单次快照看不出“死活”,必须对比:
- 快照 A:页面加载完成,组件挂载后立即拍摄
- 快照 B:打开并关闭一次模态框后拍摄
- 快照 C:重复三次操作后拍摄
- 切换到 Comparison 视图 → 筛选 Retained Size 增长明显 的 (closure) 条目
- 如果某闭包的 Retained Size 从 0.8MB → 2.1MB → 4.7MB 持续上涨,而它本应随每次关闭而释放,就坐实了泄漏

















