前端内存泄漏如何排查?常见泄漏场景有哪些?
简化版
前端内存泄漏是指不再需要的对象仍被引用,无法被 GC 回收,导致页面越用越慢甚至崩溃。常见原因包括未清理定时器、事件监听、全局缓存、闭包引用、大 DOM 被 JS 持有、订阅未取消、组件卸载后异步回调仍更新状态。排查通常用 Chrome DevTools 的 Memory 面板做 Heap Snapshot、Allocation Timeline,对比操作前后对象是否持续增长。
详细版
排查内存泄漏的关键是判断“对象为什么还活着”。浏览器 GC 会从根对象出发标记可达对象,只要还有引用链,对象就不会释放。
| 场景 | 泄漏原因 | 修复方式 |
|---|---|---|
| 定时器 | 回调闭包持有组件数据 | 卸载时 clear |
| 事件监听 | DOM 或 window listener 未移除 | removeEventListener |
| 全局缓存 | Map 长期保存对象 | 设置上限或 WeakMap |
| 异步请求 | 回调引用已卸载组件 | AbortController 或状态判断 |
useEffect(() => {
const onResize = () => {}
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])
内存排查不要只看“用了多少 MB”,更要看重复操作后内存是否持续回不到基线。
完整版教学
一、什么是前端内存泄漏
内存泄漏不是内存用得多,而是不该继续存在的对象仍然被引用。比如用户关闭弹窗后,弹窗 DOM、事件回调和数据还被全局数组引用,它们就无法被回收。
大型单页应用尤其容易遇到这个问题,因为页面不刷新,组件反复进入和离开,泄漏会慢慢累积。
二、GC 的核心判断
JavaScript 引擎会从根对象出发查找可达对象。常见根对象包括 window、当前调用栈、闭包、DOM 引用、定时器队列和事件监听。
只要一个对象还能通过引用链访问到,GC 就不会回收它。因此排查泄漏时要找 retaining path,也就是保留它的引用路径。
三、常见泄漏场景
常见问题非常朴素:
setInterval没有清理。window或document上的事件监听没移除。- WebSocket、事件总线、RxJS 订阅没取消。
- 全局数组、Map、缓存无限增长。
- 闭包保存大对象。
- DOM 删除后仍被 JS 变量引用。
- 组件卸载后异步回调仍保存状态。
四、如何用 DevTools 排查
Chrome DevTools 的 Memory 面板常用三种方式:
| 工具 | 用法 | 适合问题 |
|---|---|---|
| Heap Snapshot | 拍快照并比较 | 找对象引用链 |
| Allocation instrumentation | 录制分配过程 | 找操作中分配的对象 |
| Performance monitor | 观察 JS heap 趋势 | 看是否持续增长 |
排查步骤通常是:打开页面,记录基线;重复进入离开某功能 5 到 10 次;手动触发 GC;拍快照对比;查看增长对象和 retaining path。
五、框架里的清理模式
React、Vue 都提供生命周期清理点。React 用 useEffect 返回清理函数,Vue 用 onUnmounted。
import { onUnmounted } from 'vue'
const timer = window.setInterval(() => {
// polling
}, 1000)
onUnmounted(() => {
window.clearInterval(timer)
})
凡是“注册了外部资源”的地方,都应该有对应清理。
六、缓存也会制造泄漏
很多泄漏不是忘记清理,而是缓存策略不设边界。比如用 Map 缓存接口响应、图片对象或组件实例,时间一长就无限增长。
可以考虑:
- LRU 上限。
- 过期时间。
- 页面离开时清理。
- 弱引用场景使用
WeakMap。
七、常见误区与追问
- 误区:内存高就是泄漏。 浏览器可能为了性能暂不回收,关键看重复操作后是否持续增长且回不到基线。
- 误区:框架会自动清理所有东西。 框架能清理自身组件树,但清不了你注册到 window、事件总线或定时器里的引用。
- 误区:只要把 DOM 删除就释放了。 如果 JS 变量、闭包或缓存还引用 DOM,它仍然无法回收。
- 追问:Heap Snapshot 看什么? 看增长对象、构造函数、retained size 和 retaining path。
- 追问:为什么 WeakMap 有帮助? WeakMap 的 key 不阻止 GC,适合给对象附加元数据。
- 追问:异步请求怎么避免泄漏? 用 AbortController 取消请求,或在回调中判断组件是否仍然存活。
八、加强记忆
内存泄漏可以记成“对象还被谁拽着”。排查时重复操作、回收、拍快照、看引用链;修复时清理定时器、监听、订阅、缓存和异步任务,让对象离开页面后真正不可达。