$$('*').length每次闭环操作后线性增加即确认DOM泄漏,因其精准统计含Shadow DOM和iframe的全部节点;Performance面板Nodes曲线持续抬高且回落失败、Heap Snapshot中Detached节点Delta为正并具明确Retainers路径,共同构成泄漏铁证。

组件挂载后 DOM 节点数持续上涨,不是“可能泄漏”,而是已经泄漏——$$('*').length 每次闭环操作后线性增加,就是最硬证据。
怎么用 $$('*').length 一眼确认泄漏
别看任务管理器内存 MB 数,它被 GPU 缓存、字体、纹理干扰太强。真正敏感的是节点总数,且 $$ 能覆盖 Shadow DOM 和 iframe 内部节点,document.querySelectorAll('body *').length 会漏掉一半。
- 打开 DevTools Console,执行
$$('*').length记下基线(比如 5124) - 执行一次完整挂载 → 渲染 → 卸载闭环(如:点击按钮打开弹窗 → 数据加载完成 → 点关闭 → 等待 2 秒)
- 再跑
$$('*').length,若变成 5891,重复三次后是 6657 → 7423 → 8189,差值稳定在 +760 左右,就是典型泄漏信号 - 注意:单次不回落不算问题;连续 3 次差值递增,且增幅与组件实例数匹配(比如每开一个弹窗多出 380 个节点),基本锁定根源
Performance 面板里盯 Nodes 曲线,比看 JS Heap 更早暴露问题
JS 堆增长慢、GC 有延迟,但 Nodes 是浏览器原生 DOM 引用计数,只要节点没释放,曲线立刻拉高。它能告诉你泄漏发生在哪一步,而不是“有没有”。
- Performance 面板勾选
Memory和Screenshots,录制前先点垃圾桶图标手动 GC - 只做目标操作:比如触发组件挂载 → 等待渲染完成 → 触发卸载 → 等待 2 秒
- 停止后重点看灰色柱状图
Nodes:如果每轮峰值都比上一轮高,且结束时无法回落到初始 ±50 范围内,就是泄漏铁证 - 特别警惕“拖尾”:曲线缓慢下滑、1 秒后还卡在高位,说明 Detached 节点正被 JS 强引用着,没进 GC 队列
用 Heap Snapshot Comparison 定位 Detached 节点的保留路径
泄漏节点往往已脱离文档树,却因闭包、全局 Map 或定时器回调被 hold 住。堆快照 Comparison 视图是唯一能顺藤摸瓜的方式。
立即学习“前端免费学习笔记(深入)”;
- 拍 Snapshot #1(空闲态),挂载组件并完成卸载后立刻拍 Snapshot #2
- 切 Comparison 视图,在
Constructor列筛选HTMLDivElement、Text、Comment,看Delta是否显著为正(比如 +427) - 点开任一 Detached 行,右侧
Retainers里找链路:
– 若是Closure → function → (anonymous) → element,说明事件监听器闭包没清理
– 若是window.uiCache → Map → key → detached div,说明全局缓存没删键
– 若是setInterval → closure → componentInstance,说明定时器没清,连带锁住了整个组件作用域
组件卸载时哪些清理动作容易被忽略
不是写了 removeChild 或 el.remove() 就万事大吉。现代框架或手写逻辑中,以下清理常被跳过:
-
IntersectionObserver和ResizeObserver实例必须调用.disconnect(),否则其内部回调闭包会持续持有目标元素 -
iframe元素不能只.remove():现代浏览器仍残留HTMLIFrameElement和Detached HTMLDocument;正确流程是先设iframe.src = "about:blank",再用requestAnimationFrame(() => { iframe.contentWindow?.document.write(""); iframe.contentWindow?.close(); iframe.remove(); }) -
addEventListener绑定的箭头函数无法被removeEventListener找到——每次都是新函数;必须用具名函数或useCallback且依赖数组写全,否则清理失效 - 第三方库(如图表、富文本编辑器)常自带销毁方法(如
chart.destroy()、editor.destroy()),不调就等于留了个内存锚点
真正麻烦的不是发现泄漏,而是 Detached 节点背后那个看不见的引用链——它可能跨了三层闭包、两个模块、一次 postMessage 通信。越晚定位,越容易误判成“框架问题”。



















