$$('*').length是真在漏节点,它统计全部DOM节点(含Shadow DOM、iframe),数值线性上涨即坐实泄漏;Performance面板Nodes曲线验证回落趋势,Memory面板Heap Snapshot Comparison结合Retaining path定位Detached节点及闭包引用根源。

$$('*').length 是不是真在漏节点
直接执行 $$('*').length 看数字是否线性上涨,比盯着任务管理器内存条靠谱十倍。它统计的是全部 DOM 节点(含 Shadow DOM、iframe 内部),结果真实、无延迟。
- 打开 DevTools → Console,记下初始值,比如
124789 - 做一次闭环操作:打开弹窗 → 关闭 → 等 1–2 秒
- 再跑
$$('*').length,如果变成125203,重复三次每次 +400 左右,基本坐实泄漏 - 别用
document.querySelectorAll('body *').length替代——它漏掉 iframe 和 Shadow DOM,而现代 UI 框架和游戏 UI 常依赖这两者
Performance 面板里 Nodes 曲线怎么看
Nodes 曲线反映的是当前存活 DOM 节点总数,不是 JS Heap,也不是内存 MB 数。它对泄漏更敏感,因为 DOM 引用计数是即时的,GC 还没来得及跑,曲线已经拉高了。
- Performance → 勾选 Memory 和 Screenshots → 点 ● 开始录制
- 只做目标操作:比如切换 Tab、加载万级列表、进入战斗场景 → 点 ■ 停止
- 重点看灰色柱状图 Nodes:操作刚结束时峰值是否逐轮抬高?等待 2 秒后是否回落不到基线?回落缓慢拖尾也危险
- 如果 Nodes 涨而 JS Heap 平稳,大概率是纯 DOM 引用残留(比如
window.cacheMap存了已移除的div);如果两者同步涨,闭包 hold DOM 的可能性更高
Memory 面板堆快照怎么定位 Detached 节点
泄漏节点往往已从文档树移除,却因 JS 强引用卡在内存里,表现为 Detached HTMLDivElement 类型。Heap Snapshot Comparison 是唯一能顺藤摸瓜的方式。
- Memory → Take heap snapshot(Snapshot #1)→ 执行疑似泄漏操作(如渲染并销毁一个虚拟滚动容器)→ 手动点垃圾桶触发 GC → 再拍 Snapshot #2
- Comparison 视图 → Constructor 列筛选
HTMLDivElement、Text、Comment→ 勾选Show detached elements only - 看 Delta 是否显著为正(比如 +187),点开某行 → 右侧 Retainers 里找链路终点:
Closure → function → (anonymous) → element或window.cacheUI → Map → key → detached div - 常见陷阱:
IntersectionObserver实例没调disconnect();setInterval回调里引用了组件实例;全局缓存没清理键值
闭包持有 DOM 的修复关键点
闭包本身不泄漏,泄漏的是它捕获的 DOM 节点。问题不在“用了闭包”,而在“闭包生命周期没对齐 DOM 生命周期”。
立即学习“前端免费学习笔记(深入)”;
-
addEventListener绑定箭头函数或内联函数,removeEventListener会失效——新旧函数引用不同,旧监听器还在挂着;改用具名函数或{ signal: controller.signal }配合AbortController - 定时器回调里避免直接读
this.xxx或响应式状态;改用参数传入必要字段,或用useRef存快照值 - 组件卸载时必须显式清理:
clearInterval、controller.abort()、observer.disconnect(),且变量要定义在闭包外 - 别把整个容器节点塞进闭包(比如
document.querySelector('#app')),优先传具体 target 或用WeakMap缓存关联数据
Detached 节点数量稳定,但 Closure 构造器 Delta 持续上升——那是闭包本身在积压,背后很可能还藏着未释放的 DOM。



















