DOM泄漏本质是节点该销毁却不销毁,可通过$$('*').length初筛、Performance面板看Nodes曲线回落、Memory面板比对快照找Detached节点及Retainers路径、console.dir($0)和getEventListeners($0)定位引用源来系统排查。

高性能游戏场景下,DOM泄漏不是“节点多了就漏”,而是“节点该销毁却不销毁”——尤其在频繁创建/销毁UI弹窗、技能特效容器、HUD组件时,$$('*').length 每次操作后稳定上涨,基本就能断定泄漏已发生。
用 $$('*').length 快速确认是否真在漏节点
这是最轻量、最不可替代的初筛手段,比看内存 MB 数靠谱十倍。
- 打开 Chrome DevTools → Console,执行
$$('*').length记下基线值(比如 1842) - 执行一次闭环操作:进入战斗场景 → 触发 3 个技能弹窗 → 退出战斗 → 等待 1 秒
- 再执行
$$('*').length,若变成 2156,且重复 3 次后每次 +280~320,就是明确泄漏信号 - 注意:
document.querySelectorAll('body *').length不计入 Shadow DOM 和 iframe 内节点,而$$是 DevTools 原生 API,结果更真实
Performance 面板抓 Nodes 曲线看回落趋势
它不告诉你哪行代码错,但能一锤定音:泄漏是不是真的在发生、发生在哪一步。
- 打开 Performance 面板 → 勾选
Memory和Screenshots - 点击 ● 开始录制 → 立即执行完整闭环动作(如:进副本 → 打 BOSS → 出副本 → 等待 2s)
- 停止 ■ 后,重点看灰色柱状图
Nodes:如果每次操作后峰值都比前一次高,且结束时无法回落到初始水平,就是典型泄漏 - 别只盯“内存 MB 数”——DOM 节点本身 native 内存开销小,
Nodes曲线比 JS Heap 更敏感;DOM 渲染有延迟,等 1–2 秒再看回落值
Memory 面板拍堆快照,专找 Detached HTMLDivElement 及其保留路径
泄漏节点往往已从文档树移除,却因 JS 引用卡在内存里,表现为 Detached 类型。这是定位根源的唯一可靠方式。
立即学习“前端免费学习笔记(深入)”;
- Memory 面板 → 选 Heap snapshot → 点 Capture heap snapshot(Snapshot #1)
- 执行疑似泄漏操作(如:释放一个技能,生成并销毁 50 个
<div class="effect">) - 立刻拍第二张(Snapshot #2)→ 左上角切到 Comparison 视图
- 在 Constructor 列筛选
HTMLDivElement、Text、Comment,看 Delta 是否为正且数量异常(比如多出 472 个HTMLDivElement) - 点开任一
Detached节点,在右侧 Retainers 里顺藤摸瓜:若出现Closure → function → (anonymous) → element,说明是事件监听器里的闭包没清理;若指向window.effectCache,就是全局缓存惹的祸
用 console.dir($0) 和 getEventListeners($0) 定位具体元素引用源
console.log($0) 显示的是渲染快照,console.dir($0) 才暴露真实 JS 引用链——包括 onclick、dataset、__vue、__reactFiber 等非标准字段。
- 在 Elements 面板选中一个疑似残留的技能弹窗
<div class="popup-skill">,执行console.dir($0) - 特别检查
onmouseenter、onanimationend是否还挂着函数;留意dataset.refId或myCustomRef这类手动挂载的引用 - 再执行
getEventListeners($0):若返回{ animationend: [f, f] },说明有两个同类型监听器未清理;若函数显示为function () { }且无名,大概率来自匿名闭包,需回溯绑定位置 - 注意:Shadow DOM 内部监听器不会出现在普通
getEventListeners结果中,得切换到对应shadowRoot单独查
复杂点在于:游戏 UI 往往混合了 Canvas 渲染和 DOM 元素(比如 HUD 文字层、血条 overlay),这些 DOM 节点容易被误认为“只是辅助”,但只要被 requestAnimationFrame 回调、IntersectionObserver 或全局状态管理器(如 Redux store 的 selector)强引用,就会锁住整棵子树。最容易被忽略的是——你删掉了父容器,却忘了清空它内部的 setTimeout 或 setInterval,而这些定时器闭包又反向持有了父容器。



















