我们在写 Vue/React 组件时,常常下意识地认为“组件销毁了,占用的内存自然就释放了”。但现实往往没这么简单,这种想法有时过于天真了。
让我们先还原一个典型的场景:你写了一个弹窗组件,里面包含一个复杂的图表。当你关闭弹窗(对应的 DOM 从页面中被移除)后,你是否在全局变量、某个未清除的定时器或者事件监听器里,依然保留着对这个图表 DOM 节点的引用呢?如果有,问题就来了。
后果就是,这个 DOM 节点变成了“分离 DOM”。它虽然已经从渲染树上剥离、在页面上不可见,但由于仍然被 JavaScript 引用着,垃圾回收器无法将其回收。更糟的是,它不仅自己占用内存,还会“拽着”它的整个子节点树,导致整棵子树都无法被释放。
面试时如果被问到“如何排查内存泄漏?”,只回答“少用闭包”未免有些空洞。今天,我们就来一次实战,手把手教你使用 Chrome Memory 面板来精准定位问题。
什么是“分离 DOM”?
你可以把整个页面 DOM 结构想象成一棵大树。
- 正常节点:长在树上的枝叶,清晰可见,并被页面管理着。
- 垃圾:被风吹落、掉在地上的枯叶。它们已经没用了,垃圾回收器(GC,也就是“扫地阿姨”)会将其清理走。
- 分离 DOM:树枝被砍断掉在了地上,但你手里还紧紧攥着这根树枝的一头。虽然它已不在树上,但因为你攥着(即存在 JavaScript 引用),扫地阿姨(GC)不敢扫走它。这就造成了实实在在的内存泄漏。
实战:制造一个内存泄漏现场
为了清晰地演示如何排查,我们先写一段经典的“泄漏代码”来制造问题:
// 模拟一个全局缓存对象(这将是罪魁祸首)
const badCache = [];
function createLeak() {
const ul = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
ul.appendChild(li);
}
// 1. 把创建好的 ul DOM 元素插入页面
document.body.appendChild(ul);
// 2. 这里的引用是“合法”的,页面正常显示
// ...
// 3. 将 ul 从页面中移除 (此时页面上看不到了)
document.body.removeChild(ul);
// 4. 💀 致命操作:把这个已移除的 ul 存到了全局数组 badCache 里
// 虽然页面上没了它,但 badCache 依然引用着它,导致 GC 无法回收!
badCache.push(ul);
}
document.getElementById('btn').onclick = createLeak;
每点击一次按钮,就会创建一个包含 1000 个 <li> 的 <ul>,然后移除并存入 badCache。反复点击,内存就会持续增长。
抓“鬼”步骤:使用 Heap Snapshot(堆快照)
打开 Chrome 开发者工具,切换到 Memory 面板。
第一步:建立基准(拍摄“案发现场”初始状态)
- 选择 Heap snapshot profiling type。
- 点击 Take snapshot 按钮。这张快照记录了操作前的内存状态。

第二步:执行泄漏操作并强制垃圾回收
- 点击页面上的按钮,执行
createLeak 函数。
- 关键步骤:点击开发者工具左上角的垃圾桶图标(Collect garbage)。这步是为了手动触发一次垃圾回收,排除掉那些“本来就可以被回收”的临时对象干扰。如果 GC 之后对象还在,那基本可以断定是真正的泄漏。
第三步:拍摄第二张快照(记录泄漏后状态)
- 再次点击 Take snapshot。
第四步:对比分析与搜索定位
- 在第二张快照(Snapshot 2)的筛选器(Filter)输入框中,输入 Detached。
- 列表中应该会出现
Detached HTMLUListElement 之类的条目。
- 实锤了! 这就是我们创建的、已被移除但未被回收的分离 DOM 节点。

顺藤摸瓜:找到引用源头
仅仅发现泄漏还不够,我们必须知道是谁持有着引用,才能从根本上解决问题。
- 在快照列表中点击那个
Detached HTMLUListElement。
- 查看下方 Retainers(保留者)面板。
- 你会看到一条清晰的引用链:
Detached HTMLUListElement
in Array (说明它在某个数组里)
... in system / Context (全局上下文)
badCache (找到“凶手”了!)
结论一目了然:是全局数组 badCache 持有着对已分离 UL 节点的引用。解决方法就是去代码里找到 badCache,要么在适当的时候清除对应项,要么直接置空整个数组(badCache = null),内存泄漏就此解决。这个过程深刻体现了对应用 内存管理 的重要性。
进阶技巧:使用 WeakRef 和 FinalizationRegistry 进行监控
在现代浏览器中,我们可以利用更先进的 API 来主动监控对象是否被预期回收,这对于编写自动化测试或复杂应用的内存治理很有帮助。
// 创建一个“终结器注册表”,当被注册的对象被回收时,它会通知我们
const registry = new FinalizationRegistry((message) => {
console.log(message); // 例如输出:“ul 元素已被回收!”
});
function demo() {
const ul = document.createElement('ul');
// ... 一些操作
// 注册监听:当 ul 被垃圾回收时,执行回调函数
registry.register(ul, “ul 元素已被回收!”);
// 后续移除对 ul 的引用...
// ul = null;
}
如果一段逻辑执行后,你预期某个对象应该被回收,但 FinalizationRegistry 的回调始终没触发,那就提示可能存在内存泄漏,需要进一步排查。
结语
内存泄漏就像房间角落积累的灰尘,平时不易察觉,但日积月累会让整个应用变得“呼吸困难”,反应迟钝。掌握 Chrome Memory 面板的使用,尤其是堆快照和 Detached 节点排查,是每一位 前端 开发者从“实现功能”迈向“性能优化”的关键一步。
别再让那些早已从页面消失的 DOM 节点,像幽灵一样继续消耗着用户设备宝贵的内存资源了。如果你想深入探讨更多前端性能优化技巧,欢迎来到 云栈社区 与更多开发者交流。