Chrome DevTools 内存快照里怎么定位闭包导致的内存泄漏?
我在做一个单页应用,发现切换页面后内存一直涨,用 DevTools 的 Memory 面板拍了快照,看到很多 Detached DOM tree 和 Closure,但不知道具体是哪段代码引起的。我怀疑是事件监听没清理,但试过 removeEventListener 也没用。
比如下面这个组件,每次进入页面都会重新创建,但退出后好像还被引用着:
function createWidget(container) {
let data = fetchHugeData(); // 返回大量数据
const handleClick = () => {
console.log(data.length); // 闭包引用了 data
};
container.addEventListener('click', handleClick);
return {
destroy() {
// 忘了移除监听器...
}
};
}
这种情况下,怎么在 DevTools 里快速找到是哪个闭包持有了 data 不放?
首先确认下你的操作步骤:
1. 在页面操作前拍个初始快照
2. 执行可能泄漏的操作(比如反复进出页面)
3. 再拍个快照,选"Comparison"模式对比
重点看这几个地方:
在快照里搜"closure",然后按retained size排序。那些size特别大的闭包就是可疑对象。点开闭包详情,看它的"retainers"链,通常会显示出是哪个变量/对象一直拽着不放。
针对你的代码例子,大概率是那个
handleClick事件监听器没清理,导致整个闭包连带data都泄漏了。虽然在快照里会显示是闭包,但根源其实是DOM事件没解绑。试试这个方法:
1. 在组件销毁时确实要调用
removeEventListener2. 更简单的办法是用WeakRef,像这样改造:
另外提个醒,有时候React/Vue组件的卸载生命周期没处理好也会导致类似问题,记得检查下框架相关的清理逻辑。
操作步骤是这样的:
先进入你的页面,让组件初始化,在 Memory 面板拍个 Heap Snapshot(快照A)。然后退出页面,触发一下 GC(DevTools 里那个带扫把的圆圈图标),再拍第二个快照(快照B)。
重点来了,选中快照B,把左侧视图从 Summary 切换成 Comparison,对比基准选快照A。这时候看 Delta 列,找那些正数且内存占用大的对象,比如你那个
data数组,肯定排在前面。点进去这个对象,看下方的 Retainers 面板,这里就是案发现场。你会看到一条引用链,通常长这样:某个 DOM 节点 -> 事件监听器 -> 闭包上下文 -> 你的
data变量。如果 DOM 节点是 Detached DOM tree,那就说明 DOM 没了,但事件监听器还活着,它死死抓着闭包不放。至于代码修复,你那个
destroy里必须得把handleClick引用存下来才能移除,匿名函数或者重新声明的函数是移除不掉的。以后遇到这种问题,记住“对比快照 + 看 Retainers 引用链”这招,基本上一抓一个准。