排查 javascript 内存泄漏时,分析 heap snapshot 的核心做法是:在 chrome devtools 中录制两次堆快照(分别对应操作前后的状态),然后在比较视图中筛选 detached 节点。通过观察这些脱离 dom 树但仍在内存中的对象,定位持有它们的闭包或全局引用,从而找到泄漏源头。

如何生成并对比两个 Heap Snapshot
要获取有效的对比数据,必须确保两次快照之间发生了预期的内存分配或释放操作。首先在 DevTools 的 Memory 面板中选择 Heap snapshot 类型,点击录制按钮开始监控。执行完触发内存变化的代码逻辑后,停止录制并保存第一个快照。接着重复执行相同的操作序列,再次录制并保存第二个快照。最后,在左侧文件列表中右键点击第二个快照,选择 Compare with... 并选中第一个快照。此时界面会展示两个时间点之间的内存差异,重点关注 Comparison 视图下的 Detached 分类,这里列出了那些不再被 DOM 引用但仍驻留在内存中的元素。

const container = document.getElementById('container');
function createLeak() {
const largeData = new Array(1000000).fill('leak');
// 模拟一个事件监听器,未移除导致泄漏
const handler = () => console.log(largeData.length);
const btn = document.createElement('button');
btn.textContent = 'Click';
btn.addEventListener('click', handler);
container.appendChild(btn);
// 模拟页面跳转或组件卸载,移除 DOM 但保留引用
setTimeout(() => {
container.removeChild(btn);
// 注意:btn 和 handler 仍然被外部作用域或全局变量隐式持有
window.leakedBtn = btn;
window.leakedHandler = handler;
}, 1000);
}
createLeak();
// 等待 1.5 秒后手动触发第二次快照对比
预期结果:在第一次快照中,btn 存在于 DOM 树中。在第二次快照中,btn 已从 DOM 移除,但由于 window.leakedBtn 的存在,它成为 Detached 节点。在对比视图中,你会看到该按钮对象及其关联的事件监听器函数未被垃圾回收。
如何通过 Retainers 路径定位泄漏根源
找到 Detached 节点只是第一步,关键在于理解为什么它们没有被回收。点击某个可疑的 Detached 对象,右侧面板会显示其 Retainers(保持者)列表。这个列表展示了从根节点(如 window、global)到该对象的完整引用链。沿着这条路径向上追溯,你可以发现是哪个全局变量、闭包或模块级状态意外地持有了该对象。通常,泄漏源于长生命周期的对象(如全局单例、定时器回调、DOM 事件监听器)短生命周期对象(如临时 DOM 元素、局部变量)。检查引用链中的中间节点,确认是否存在不必要的属性赋值或未清理的事件绑定。

// 假设我们在上面的例子中继续分析 // 在 DevTools Console 中查看 Retainers console.dir(window.leakedBtn); // 这将显示 leakedBtn 指向的 DOM 节点 // 以及它所持有的事件监听器 handler // handler 闭包捕获了 largeData 数组 // 修复方案:清除引用 window.leakedBtn = null; window.leakedHandler = null;
预期结果:通过 console.dir 或 DevTools 的 Retainers 视图,可以清晰看到 window.leakedBtn -> button element -> click event listener -> closure (largeData) 的引用链。将 window.leakedBtn 设为 null 后,断开这条链,下次 GC 即可回收相关内存。
Detached DOM Elements 的具体特征与识别
在 Heap Snapshot 的 Comparison 视图中,Detached 类别专门用于标识那些已经从文档流中移除,但仍然被 JavaScript 代码引用的 DOM 节点。这些节点的特征是它们在 DOM 树中不可见(即 parentNode 为 null),但在 JS 堆中依然存在。识别它们的关键在于区分“正常移除”和“泄漏”。如果一段代码正确地移除了 DOM 元素并且没有保留任何对该元素的引用,那么在第二次快照中,该元素不应出现在 Detached 列表中。只有当存在外部引用(如全局变量、闭包变量、缓存 Map 等)时,才会出现泄漏。此外,要注意 Window 对象本身也可能包含大量 Detached 元素,这通常意味着全局命名空间污染或未及时清理的全局监听器。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











