关键是要通过保留组定位拖住闭包的源头对象:先筛选高Retained Size的(closure)实例,用Dominators视图排序,检查Retained Size远大于Shallow Size的闭包;右键→“Retaining Group”查直接持有者(如未解绑监听器、未执行回调、未清理缓存、detached DOM等);双击进入[[Scopes]]确认捕获的过期对象;再用差分快照比对closure增长趋势与销毁组件的关联。

关键不是找“匿名函数”,而是找被它拽住不放的不该存在的对象。保留组(Retaining Group)能帮你跳过杂乱引用,直接看到谁在拖着内存不放。
先锁定高 Retained Size 的 (closure) 实例
打开 Dominators 视图,按 Retained Size 降序排列。重点看构造函数为 (closure)、且 Retained Size 明显远大于 Shallow Size 的条目(例如 2MB vs 4KB)。这类闭包大概率捕获了大量上下文数据,但本身代码量极小——说明它“拽”着别的东西。
- 不要只盯着 Summary 里几个 closure 实例数,它们可能只是冰山一角
- 如果多个 closure 共享同一份大对象(比如一个全局缓存数组),它们的 Retained Size 会叠加显示,更容易暴露
- 注意时间点:在组件卸载、页面跳转后立即拍快照,此时还活着的 closure 更可疑
用保留组反向定位“被拖累”的死对象
右键目标 closure → “Retaining Group” → 选择 “Objects retaining this object”。这时你会看到一组直接持有它的对象,比如:
- window.xxxHandler —— 全局事件监听器没解绑
- PromiseReactionJob 或 setTimeout 的内部任务队列 —— 异步回调还没执行完
- 某个静态 Map 或 Set 的 entry —— 手动缓存未清理
- 已 detached 的 DOM 节点或 React 组件实例 —— 说明闭包本该随组件销毁,却还挂着
这些就是“拽着”闭包不放手的源头。顺着它们再点开,往往就能看到闭包实际捕获的 state、ref、大数组等。
在 [[Scopes]] 中确认捕获内容
双击 closure 进入详情,展开右侧的 [[Scopes]] 节点。这里列出它词法作用域中所有被引用的变量绑定:
- 找指向 已 unmount 的组件 this、ref.current 已为 null 的 DOM 节点、旧版本的大型 response.data 等明显过期的对象
- 如果 scope 里有 arguments 或 eval 相关字段,要格外小心——它们可能隐式扩大捕获范围
- 注意 closure 名称为空或显示为 anonymous,不代表它没名字;真正重要的是它持有什么,而不是它叫什么
交叉验证:差分快照抓增长证据
单次快照只能看“现在谁活着”,但看不出“为什么不该活”。必须做对比:
- 快照 A:页面加载完成、组件挂载后立刻拍摄
- 快照 B:打开并关闭一次模态框后拍摄
- 快照 C:重复三次操作后拍摄
切换到 Comparison 视图,筛选类型为 (closure) 的新增项。如果某 closure 的 Retained Size 在 B→C 之间持续增长,且其 [[Scopes]] 始终指向同一个已销毁组件,基本可断定是异步匿名函数导致的驻留。

















