Detached DOM 内存泄漏源于节点移除后仍被闭包、事件监听器或 JS 引用持有;需通过 Heap Snapshot 查 Detached 节点及保留路径,重点检查未解绑的全局/动态/第三方监听器,以及全局变量、异步引用、WeakMap 误用和视图迁移逻辑错误。

DOM 节点脱离(Detached DOM)却未被释放,是 SPA 中典型的内存泄漏表现:节点已从文档树移除,但因闭包、事件监听器或 JS 引用仍被持有,导致无法被垃圾回收。排查需聚焦“谁还连着它”,而非只看是否还在页面上。
确认是否存在 Detached DOM
打开 Chrome DevTools → Memory 面板 → 点击 “Take Heap Snapshot” → 在筛选框输入 Detached;若结果中出现大量 HTMLDivElement、HTMLVideoElement 或自定义组件类名,说明存在脱离但未释放的节点。可点击展开查看保留路径(Retaining Path),重点看“Closure”“EventListener”“Property”等关键词指向的 JS 对象。
检查事件监听器是否残留
Detached DOM 最常见成因是监听器未解绑。尤其注意以下三类绑定:
- window 或 document 上的全局监听(如 resize、scroll、keydown),在路由离开时未调用 removeEventListener
- 动态插入的 DOM 元素(如弹窗、下拉菜单)上绑定的事件,其回调函数通过闭包引用了组件实例或大型数据
- 第三方库(如日期选择器、富文本编辑器)内部自动注册的监听,未随组件卸载而销毁
验证方式:Elements 面板选中 → 右侧 Event Listeners 标签 → 展开对应事件类型,查看监听器是否仍挂载在已隐藏/切换走的元素上。
排查 JS 引用链未断开
即使 DOM 被移除,只要 JS 代码仍持有它的引用,它就无法回收。常见场景包括:
- 将 DOM 节点赋值给全局变量或模块级常量(如
let cachedNode = document.getElementById('xxx')) - 在定时器、Promise 回调或异步操作中持续引用该节点(如
setTimeout(() => node.style.color = 'red', 1000)) - 使用 WeakMap 存储节点相关状态,但 key 是节点本身——若节点已 detach 却 map 未清理,WeakMap 不会自动删除条目(WeakMap 的 key 是弱引用,但 value 若含对节点的强引用,仍会阻止回收)
验证视图切换逻辑是否误移节点
在 EJS 或原生 JS 构建的 SPA 中,常见错误是用 layout.append(view.childNodes) 迁移内容。这会把子节点从原容器中移动出去,导致原容器变空;再次路由时 view.childNodes 为空数组,视图区域不再渲染任何内容,看似“丢失”,实则是节点被移走后未重建,后续也无新节点可插入。正确做法是 layout.append(view) —— 整个 view 元素被移动,结构和状态完整保留,且每次都能被 querySelector 正确定位。
不复杂但容易忽略



















