保留组是对象被回收后所有只能经由它访问的对象构成的完整子图;JS中匿名闭包常因意外持有外层变量形成隐蔽保留组,导致内存泄漏。

保留组(Retained Set)不是独立指标,而是“保留大小”背后的逻辑集合——它指一个对象被回收后,**所有只能经由它访问到的对象所构成的完整子图**。在 JavaScript 堆快照中,匿名函数((closure))常因意外持有外层作用域变量而形成隐蔽的保留组,导致本该释放的对象长期驻留内存。要快速识别这类问题,关键不是找“闭包本身”,而是看它拖住了什么、为什么活下来。
聚焦“(closure)”构造器下的高保留大小实例
在 Chrome DevTools 的堆快照 Constructor View 中,直接搜索或展开 (closure) 类型。不要只看数量,重点排序其 Retained Size 列:
- 单个闭包 Retained Size 超过 1MB,尤其伴随大量
Array、Object或string被支配,高度可疑 - 多个同名闭包(如来自同一事件处理器或定时器)Retained Size 累计占总堆 5% 以上,说明存在重复累积
- 闭包自身 Shallow Size 很小(通常几 KB),但 Retained Size 异常大——这是典型“轻量入口、重量拖拽”特征
顺着“支配者”(Dominators)定位源头闭包
切换到 Dominators 视图,按 Retained Size 降序排列。真正起阻塞作用的闭包往往出现在顶部几行,因为它支配着下游大片内存:
- 点开一个高 Retained Size 的闭包,查看右侧的 Retained Size breakdown,确认哪些对象被它唯一持有(例如:一个未清理的
Map实例、一个缓存的 DOM 节点树、或一整段 JSON 解析结果) - 若该闭包的支配路径终点是
window、document或某个全局模块对象,说明它已被提升为长期引用 - 特别注意闭包引用链中是否出现
setTimeout/setInterval回调、未解绑的addEventListener、或 Promise 链中的then处理器——这些是匿名函数泄漏的高频位置
检查闭包的“作用域链”确认引用合理性
右键目标闭包 → Reveal in Summary view → 在下方 Sources 面板中点开其 Scope 标签:
- 观察 Closure 作用域下是否包含明显不该长期存活的变量:如整个
response.data、未销毁的图表实例、或已卸载组件的this上下文 - 若作用域中出现
__webpack_require__、module或大型模块对象,说明打包器未做 tree-shaking,或 HMR 热更新残留了旧模块引用 - 对比正常生命周期:一个仅用于一次点击处理的闭包,其作用域应只含几个基本类型或短生命周期对象;若看到数组长度上千、字符串超百 KB,基本可判定异常
用对比快照验证闭包是否“不该活”
单个快照无法判断是否泄漏,必须结合操作流:
- 在页面加载后拍第一张快照(Baseline)
- 执行一次触发匿名函数的操作(如打开弹窗、提交表单),再拍第二张
- 执行反向操作(关闭弹窗、返回上页),等待 2–3 秒后拍第三张
- 切换到 Comparison 视图,筛选
(closure),查看第三张快照中是否有大量新增且 Retained Size 不降反升的实例

















