浏览器内存泄漏排查
通过可重复场景、GC 后基线、前后 Heap Snapshot、Retainers 和 allocation trend 判断浏览器内存增长是否为真正泄漏,并定位未释放监听器、Detached DOM、定时器、闭包和无界缓存。
[!info] related notes
- 所属 MOC: 浏览器开发者工具 MOC
- 工具: Memory 面板, Performance 面板, Sources 面板
- 基础: JavaScript 垃圾回收与内存管理
- 监控: 性能监控体系
浏览器内存泄漏排查
先定义什么叫泄漏
内存上涨不等于泄漏。
可能只是:
- 正常 cache;
- lazy-loaded data;
- 浏览器/V8 预留 heap;
- GC 尚未发生;
- 用户确实打开了更多内容。
更有意义的泄漏证据是:
执行同一可逆操作多轮,并回到逻辑基线后,本应可回收的对象仍持续被保留。
建立可重复场景
选择一个可重复、最好可恢复的动作:
baseline
→ open dialog/page/chart
→ close/unmount
→ return to baseline
重复多轮。
只有这样才能把“功能正常占用”与“每轮额外残留”分开。
三阶段快照思路
对可逆场景可使用:
- baseline snapshot;
- 执行动作后的 snapshot;
- 退出/卸载并 GC 后的 snapshot。
第三份比第二份更关键:它回答“已经回到逻辑基线后,什么还不该活着却活着”。
找持续增长对象
在 Comparison / statistics 中关注:
- 某 class/component 实例数量每轮增加;
- Detached DOM nodes;
- 大 Array/Map/Set;
- listener / closure 相关对象;
- 某第三方库对象。
不要只看总 heap MB。
用 Retainers 找真正根因
泄漏的关键问题不是:
这个对象占多少?
而是:
谁还在持有它,使 GC 无法回收?
沿 Retainers 可能找到:
- window/document event listener;
- timer callback;
- global store/cache;
- closure;
- framework registry;
- detached tree 中的引用。
Allocation Trend
如果快照很难看出分配来源,可记录 allocation trend / sampling,找到哪些调用栈持续产生对象。
Allocation sampling 通常比完整 allocation instrumentation 更轻量,但它仍然是诊断数据,不等于生产 RUM 的通用替代。
高频根因
Event listener
组件卸载但全局 listener 没 remove。
Timer
setInterval / 长期 timer 的 callback 保留闭包。
Detached DOM
DOM 已从 document 移除,但 JS 变量/observer/listener 仍引用。
无界缓存
Map/Array 只增不删,没有 TTL/LRU/容量上限。
Subscription
WebSocket/event bus/store subscription 没 unsubscribe。
修复后必须复测
用相同操作重新跑:
baseline
→ repeat N times
→ return
→ GC
→ snapshot
目标不是让 heap 完全回到同一个字节数,而是:
- 可疑实例不再线性累积;
- retain path 消失;
- 内存增长趋于稳定。
迁移检查
场景:一个 Chart Dialog 每打开关闭 10 次,heap 都高一点。
闭卷完成:
- 为什么只看 Task Manager 内存曲线不够?
- 怎样建立可重复 baseline?
- 为什么需要操作后的快照和回到 baseline 后的快照?
- 如何用 Retainers 区分 listener、timer、global cache 三种根因?
- 修复后怎样证明“不是碰巧 GC 了”,而是真正停止累积?