2026 祝大家元旦快乐
内存泄漏
内存泄漏:程序未能释放已经不再使用的内存,导致内存占用持续增长,影响系统性能甚至导致进程崩溃。
关键点:内存无法被正常释放,如果是正常/合理的内存占用,或者后续能正常释放内存,不算内存泄漏。
常见的内存泄漏情况:
- 组件内未释放的定时器、事件监听、插件实例:组件销毁时没有清理定时器、事件监听器。
- 意外全局变量 :全局声明,函数内赋值,或者
this属性意外指向全局变量,在离开时没有解除引用。 - 闭包函数:闭包本身不属于内存泄漏(合理内存占用),闭包意外引用而在后续想销毁无法销毁的情况下才算内存泄漏。
- 创建后没有清理的 DOM 元素:DOM 元素被引用但未及时清理。
- URL.createObjectURL() 未释放 :使用
URL.createObjectURL()创建对象 Blob URL 后,需要调用URL.revokeObjectURL()释放内存。
内存泄漏(Memory Leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。
并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存。
对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
内存分析手段
前端内存分析常用两种方式:Performance 性能分析面板和 Memory 堆内存分析。
Performance 面板分析
Performance 面板是常见的性能和内存分析手段,可以简单地确认是否存在内存泄漏情况,但不能作为主要依据。
流程:
- 打开 Performance 面板。
- 选择点击内存模块(图中 1)开启内存视图,执行一次垃圾回收(图中 2),避免其他影响。
- 运行性能检测(图中 3),执行可能存在的内存泄漏的操作(页面切换、组件挂载、弹窗、事件等)。
- 完成后,找到 JS Heap(图中 4):
- 如果 JS Heap 曲线一直上升,则可能存在内存泄漏问题。
- 如果 JS Heap 曲线有升有降,就是正常的垃圾回收操作。
注意:因为 V8 在初始加载过程中会不断对内存进行扩容,或者异步组件实例化时会占用多余的内存,只能确认可能存在内存泄漏。

堆快照分析
通常在上述 Performance 性能分析后,就可以定位到大致的内存泄漏进行解决。
但是有时候会难以确认具体的代码段,这时候就需要借助堆快照来完成。堆快照可以看到实际的内存占用和对象引用情况,可以更方便确认泄漏来源。
堆快照面板:

内存堆分析工具中的关键术语:
- Constructor(类名):对象的构造函数名称。
- Distance(引用层级):对象到 GC 根节点的引用层级。
- Shallow Size(自身大小):对象自身占用的内存大小。
- Retained Size(保留大小):自身 + 引用内容大小(包含了外部引用对象)。
- Object Count(对象数量):该类型对象的数量。

- Retainers(引用关系):具体对象引用层级关系,用于追踪对象被哪些对象引用。
使用流程:
- 打开 Memory 面板。
- 执行一次垃圾回收,录制 Heap Snapshot(Snapshot A)。
- 执行可能存在内存泄漏的操作。
- 再执行一次垃圾回收,录制 Snapshot(Snapshot B)。
- 对比 Retained Size / Object Count ,如果 Object Count 持续增加,而且 Retained Size 无法释放,那该类对象可能存在内存泄漏情况。
代码段定位流程:
- 找到 Retained Size 中的最大实例:在内存堆快照中,找到 Retained Size 最大的对象实例。
- 在 Retainers 中找到
context in定位源码进行优化 :通过 Retainers 面板查看对象的引用关系,找到context in标记,定位到源码位置进行优化。
这个方案可以快速确认内存来源,但不一定准确,更多的是确认可能存在内存占用过多的情况。

内存泄漏常见解决方案
定时器、监听事件泄漏、全局引用泄漏
在离开 Vue 组件时,beforeDestroy / beforeUnMount 钩子中:
- 销毁所有监听器。
- 销毁所有定时器。
- 移除 DOM、全局对象、插件的引用:
obj = null。如果是插件,执行插件内部的销毁函数。
闭包存在内存泄漏
根据闭包泄漏的可能情况来避免:
- 闭包内部引用了全局变量:闭包持有全局变量的引用,导致变量无法被回收。
- 引用了 DOM 节点:即事件监听的情况,引用了某个对象变量。
- 定时器内部存在的闭包引用:即定时器本身的情况,闭包持有定时器的引用。
- 闭包循环引用:闭包和对象之间形成循环引用,导致 GC 无法回收。
示例:闭包与对象循环引用
javascript
// 示例:闭包与对象循环引用
function createCycle() {
var obj = { name: "test" };
var bigData = new Array(1000000).fill("cycle data");
// obj.closure(闭包)引用了 obj,obj 又持有 closure 的引用,形成循环。GC 无法判断这组引用是否 "有用",会一直保留在内存中。
obj.closure = function () {
console.log(bigData.length, obj.name); // 闭包引用 bigData 和 obj
};
// 循环引用:obj 持有闭包,闭包持有 obj
return obj;
}
var cycleObj = createCycle(); // cycleObj 持有 obj 引用
// 即使后续不再使用 cycleObj,循环引用导致 bigData 和 obj 无法回收
Chrome DevTools 工具
在打开 DevTools 时,console 对象会持有引用,可能存在泄漏。
javascript
const obj = { large: new Array(1000000) };
console.log(obj);
这里 console 会保存 obj 的引用,方便我们后续展开对象查看属性,或者通过 $0、$1 再次访问。
这个设计实际上不是内存泄漏,只是内存占用过多的场景,可能会影响后续内存分析,只要关闭 DevTools 再打开就能正常。
Map 和 Set
已知 Map 和 Set 会对存储的对象持有强引用,如果没有正确管理的话,会导致对象无法被正常释放,存在内存泄漏。
这类场景下,可以使用 WeakMap / WeakSet 进行优化。WeakMap 的 key / WeakSet 的值是弱引用对象,当对象没有被其他强引用持有的时候,会被在适当的时候被垃圾回收器回收,避免强引用导致的内存泄漏。
所以 WeakMap / WeakSet 中的对象数量是不可预测的,因此不会提供迭代和 size 长度查询这些接口。
总结
- 内存泄漏核心概念:内存无法被正常释放,如果是正常/合理的内存占用,或者后续能正常释放内存,不算内存泄漏。
- 内存分析手段 :
- Performance 面板分析:通过 JS Heap 曲线判断是否存在内存泄漏,曲线一直上升可能存在泄漏,有升有降为正常垃圾回收。注意 V8 初始加载和异步组件实例化会占用多余内存,只能作为初步判断。
- 堆快照分析:通过对比多个快照的 Retained Size 和 Object Count,定位具体泄漏来源。
- 代码段定位流程 :
- 找到 Retained Size 中的最大实例。
- 在 Retainers 中找到
context in定位源码进行优化。
- 解决方案 :
- 定时器、监听事件、全局引用泄漏 :在组件
beforeDestroy / beforeUnMount钩子中销毁所有监听器、定时器,移除 DOM、全局对象、插件的引用。 - 闭包内存泄漏:避免闭包内部引用全局变量、DOM 节点、定时器,避免闭包循环引用。
- Map 和 Set 强引用:使用 WeakMap / WeakSet 替代,利用弱引用特性避免强引用导致的内存泄漏。
- Chrome DevTools 影响:注意 console 对象会持有引用,可能影响内存分析,关闭 DevTools 再打开即可恢复正常。
- 定时器、监听事件、全局引用泄漏 :在组件