内存泄漏是前端性能优化中最难排查的问题之一。本文将详细介绍如何使用 Chrome DevTools 的 Performance 和 Memory 面板,系统化地排查并定位内存泄漏问题,并映射到真实代码逻辑。
- 页面运行一段时间后变卡
- 滚动、点击响应变慢
- 浏览器标签页显示内存占用持续增长
- 最终页面崩溃(Out of Memory)
在控制台运行以下代码,观察内存是否持续增长:
setInterval(() => {
const usedMB = performance.memory.usedJSHeapSize / 1024 / 1024
console.log(`当前内存占用: ${usedMB.toFixed(2)} MB`)
}, 1000)
- 打开 DevTools (F12) → Performance 标签
- 勾选 Memory 选项
- 点击 Record 录制 30-60 秒
- 执行可疑操作(滚动列表、打开关闭弹窗等)
- 停止录制
正常情况(有涨有跌,GC 能回收):
Memory (MB)
↑ ╱╲ ╱╲ ╱╲
│ ╱ ╲ ╱ ╲ ╱ ╲
└─────────────────────────→ 时间
内存泄漏(持续上涨,呈阶梯状):
Memory (MB)
↑ ╱──────╱─────╱─────╱──
│ ╱ ╱ ╱ ╱
└─────────────────────────→ 时间
判断标准:
- ✅ 正常:内存有涨有跌,GC 后能降下来
- ❌ 泄漏:内存持续上涨,GC 后仍然增长
操作流程:
- DevTools → Memory 标签 → 选择 "Heap snapshot"
- 点击 "Take snapshot" → 获得快照 1
- 执行可疑操作(如打开 10 次弹窗后关闭)
- 强制垃圾回收(点击 🗑️ 图标)
- 点击 "Take snapshot" → 获得快照 2
- 切换视图为 Comparison → 选择 "between Snapshot 1 and Snapshot 2"
关键列说明:
| 列名 | 含义 | 关注点 |
|---|---|---|
| # Delta | 净增加的对象数量 | 应该接近 0 |
| Size Delta | 净增加的内存 | 最关键的指标 |
| Alloc. Size | 新增对象占用内存 | 持续增长说明泄漏 |
| Freed Size | 释放的内存 | 应该接近 Alloc. Size |
按 Size Delta 排序,找到占用内存最多的对象类型:
Constructor # Delta Size Delta
─────────────────────────────────────────────
(array) +500 +2.5 MB ← 可疑!数组持续增长
Detached HTMLDivElement +200 +800 KB ← DOM 泄漏
EventListener +150 +150 KB ← 事件监听器未移除
这是最关键的环节! Retainers 显示了为什么这个对象没有被垃圾回收。
操作:点击可疑对象 → 选择具体实例 → 右侧面板显示 Retainers
示例 1:全局变量引用
Retainers:
→ Window / http://localhost:3000
→ VueComponent ← Vue 组件实例
→ setupState ← setup() 返回的状态
→ allData ← 你的变量名
→ @123456 (array) ← 泄漏的数组
如何对应到代码:
- 看到
allData→ 在代码中搜索const allData = ref(...) - 看到
VueComponent→ 定位到具体的组件文件
示例 2:事件监听器引用
Retainers:
→ Window
→ eventListeners ← 全局事件监听器映射
→ scroll ← scroll 事件
→ [[Handler]]
→ VueComponent ← 组件实例被闭包引用
结论:scroll 事件监听器没有被移除,闭包引用了组件实例。
通过 Memory 面板,常见的泄漏对象类型:
现象:
Constructor # Delta Size Delta
────────────────────────────────────────────
Detached HTMLDivElement +200 +800 KB
原因:移除 DOM 时未清理事件监听器,导致元素无法被 GC
解决:在移除 DOM 前调用 removeEventListener
现象:
Constructor # Delta Size Delta
────────────────────────────────────
Timeout +50 +100 KB
原因:组件销毁时定时器仍在运行
解决:在 onUnmounted 中调用 clearInterval / clearTimeout
现象:
Constructor # Delta Size Delta
──────────────────────────────────────
EventListener +100 +200 KB
原因:全局事件监听器未移除
解决:在 onUnmounted 中调用 window.removeEventListener
现象:
Constructor # Delta Size Delta
────────────────────────────────────────
VueComponent +50 +5 MB
原因:事件总线监听器未注销,导致组件实例无法释放
解决:在 onUnmounted 中调用 bus.off
现象:
Constructor # Delta Size Delta
────────────────────────────────────
(array) +100 +10 MB
(closure) +50 +500 KB
原因:事件处理函数闭包不必要地引用了大对象
解决:只保留必需的数据,或在使用后手动设为 null
Performance 面板录制 60 秒后发现:
- JS Heap 从 50MB 增长到 150MB
- 内存呈阶梯状持续增长
- 没有明显的 GC 回收
Comparison (Snapshot 1 vs Snapshot 2):
Constructor # Delta Size Delta
────────────────────────────────────────
(array) +1 +50 MB ← data 数组持续增长
EventListener +1 +100 KB ← scroll 监听器未清理
WebSocket +1 +50 KB ← WebSocket 未关闭
通过查看 Retainers 路径,找到 3 个泄漏点:
- data 数组:
Window → VueComponent → setupState → data持续增长,没有限制大小 - scroll 监听器:
Window → eventListeners → scroll未移除,闭包引用了data - WebSocket:组件销毁时未关闭,回调闭包引用了
data
根据 Retainers 路径,需要:
- 限制
data数组最大长度 - 在
onUnmounted中移除scroll监听器 - 在
onUnmounted中关闭 WebSocket 连接 - 使用
shallowRef优化大数据响应式开销
Vue 3 组件中需要在 onUnmounted 清理的资源:
- 定时器:
clearInterval(timer)/clearTimeout(timer) - 全局事件监听:
window.removeEventListener('resize', handler) - WebSocket:
ws.close() - 第三方库实例:
chart.dispose()(如 ECharts) - 事件总线:
bus.off('event', handler) - Observer:
observer.disconnect()(IntersectionObserver/ResizeObserver) - 大数据限制:使用
shallowRef+ 限制数组最大长度
- Performance 面板 → 确认是否泄漏(观察内存走势图)
- Memory 面板 → 对比快照,找到泄漏对象类型
- 查看 Retainers → 找到引用路径
- 映射到代码 → 通过变量名定位文件和行号
- 修复 + 验证 → 清理资源,再次录制确认修复
- 看 Retainers:这是定位代码的关键,显示从 Window 到具体变量的完整路径
- 认识常见模式:定时器、事件监听器、DOM 引用、闭包是主要原因
- 使用 shallowRef:大数据场景必备,减少响应式开销
- 限制数据量:虚拟滚动中必须限制数组大小
- 清理资源:
onUnmounted中清理所有副作用
- ✅ 所有副作用都在
onUnmounted中清理 - ✅ 使用
shallowRef存储大数据 - ✅ 限制列表/数组的最大长度
- ✅ 避免闭包捕获大对象
- ✅ 定期用 DevTools 检查内存占用