1. 可能产生内存溢出的情况
现在浏览器最常使用的垃圾回收机制是标记清除,老版本浏览器常用的机制是引用计数,由于引用计数方式无法解决循环引用怎么回收的问题,所以逐渐被淘汰。其中内存溢出问题,往往是变量或函数引用未被清除,导致其所关联的dom/对象/事件/样式等占据的内存无法被回收引起的。
1.1 意外的全局变量
直接挂载到window下的全局变量,浏览器回收时会略过,因此不需要再使用变量时,要用如下方式手动释放内存,否则内存会被一直占用着
//必须要用的情况下
//手动释放全局变量的内存
window.bar = null;
delete window.bar;
1.2 被遗忘的定时器或回调函数
如果设置了setTimeout或setInterval定时器,但没有使用clearTimeout/clearInterval清除定时器,定时器的回调函数以及内部依赖的变量都不能被回收,从而造成内存泄漏。
1.3 游离的dom节点
如果删除了某个dom节点,但仍有变量对此节点存在引用关系,则这个dom就会变成游离状态,也就是不存在于document上了,但由于引用关系未消失,导致所占据的内存始终未被回收。
1.4 闭包
当闭包中存在外部引用的变量时,如果此变量占据的内存未被回收,则闭包占据的内存也无法被回收。
1.5 堆栈溢出
递归或函数调用层级过多时,可能导致内存的上溢(栈满时再做进栈必定产生空间溢出)或下溢(栈空时再做退栈也产生空间溢出)。
1.6 未及时销毁的事件注册
如果在一个dom上注册了事件,之后删除dom,但未注销事件,那么可能导致事件占据的内存未被回收。
<div id="btn">按钮</div>
<script>
const $btn = document.getElementById('btn');
const fn = () => {};
// 注册事件
$btn.addEventListener('click', fn);
// 注销事件,才能保证事件占据的内存被回收掉
$btn.removeEventListener('click', fn);
</script>
1.7 console
控制台日志记录对总体内存内置文件的影响,也是个重大的问题,同时也是容易被忽略的。记录错误的对象,可以将大量的数据保留在内存中。传递给console.log的对象是不能被垃圾回收的,所以没有去掉console.log可能会存在内存泄漏
2. 利用devtools查找问题
我们做的网页、应用在使用一段时间后,变得越来越卡,通过任务管理器查看,发现网页所在浏览器占据的内存容量越来越大,这种情况就可能是内存溢出导致的。如果怀疑网页产生了内存溢出,可以使用浏览器(chrome)自带的devtools按照下面步骤进行筛查:
2.1 通过Performance确认大体的溢出位置
先进入devtools的Performance界面,点击Record按钮进行动作录制,录制过程中操作网页动作(比如组件间的切换、获取更多列表之类的),结束录制点击Stop即可,之后界面会生成录制结果分析,勾选Memory,会出现表示内存占用的折线图,如果折线图整体走势一直在上升,而没有下降的话,基本可以确定检测的网页操作存在内存溢出。
2.2 使用Memory进行细粒度的问题分析
通过第一步大体确认某些操作存在内存溢出后,就可以通过devtools的Memory进行更细粒度的分析了,Memory界面中包含三种分析方式,分别是heap snapshot、Allocation instrumentation on timeline、Allocation sampling,具体每一项的详细介绍,可以参见掘友写的这篇文章:前端性能监控实践(二)chrome devtools。由于Allocation instrumentation on timeline收集录制结果的时间会很长,甚至有时候会卡死浏览器,所以本人基本不使用这种方式。Allocation sampling主要用来查看内存占用大小,判断哪一部分占用内存大的时候,会用到这个分析方式,但这个方式无法判断内存是否存在溢出。这里我们主要使用heap snapshot的方式定位内存溢出问题,因为生成快照的方式虽然麻烦,但可参考的信息也确实是最详细的。
2.3 根据heap snapshot,判断内存溢出的代码位置
确定使用heap snapshot方式进行细粒度分析后,首次进入页面后,先点击Take heap snapshot生成一个快照
然后找到通过Performance粗略定位到的溢出位置,进行一些操作后,再次点击Take heap snapshot生成一个快照,此时就有两个快照了,两个快照可以通过Comparison进行对比,查看多出了哪些内存占用,Comparison列表中主要看New、Deleted、Delta三项,New代表此次快照新增的内存占用数量,Deleted代表此次快照销毁的内存占用数量,Delta代表用来对比的快照增加或减少的内存占用数量,也就是New数量减去Deleted数量的结果
如果感觉两个快照太少,想多来几个做参照,则可以每操作几下就生成一个快照,最终多个快照一一进行对比
我做的是vue项目,常出现dom销毁但dom引用依然存在的问题,因此会先查看游离对象的内存占用问题,在快照列表上方的Class filter输入框中输入Detached,就会筛选出游离对象的列表
游离列表中的Detached HTMLDivElement代表游离的dom元素,点击左侧小箭头,会出现具体的游离dom信息,信息中有一项_prevClass,代表dom对应的class,如果这个class名字比较唯一,则能很容易通过这个class查找到相对应的代码组件,然后根据组件的代码逻辑,判断是否有涉及到dom的变量引用,在beforeDestroy生命周期中手动销毁这个变量(将变量设置为null),往往可以解决游离dom的问题
不过游离dom的产生,有时问题不一定出在dom所在的组件中,也可能是外部调用此组件的地方,存在无法被回收的变量或函数引用,导致相关联的多个组件产生了游离dom
3. vue项目中可能产生内存溢出的地方
vue项目中出现内存溢出,常见于下面几种情况:
- 使用mixin混入进来的函数时,可能形成一个使用不当的闭包,导致闭包占据的内存无法被回收。(使用不当的闭包常出现于闭包内部存在外部的引用类型变量的影响,可以尝试使用深拷贝切断内外变量的引用联系)
- 如果使用了原生方式绑定了事件,在组件销毁时未及时解绑事件,会导致事件占据的内存无法被回收。
- 如果使用EventBus注册了事件,没有地方销毁事件的话,也会导致事件占据的内存无法被回收。
- 如果通过双向通信技术,绑定了一个需要其他端调用的回调函数,而这个回调函数在使用完后,没有手动销毁(置成null),那么这个回调函数占据的内存无法被回收,甚至可能因为形成了使用不当的闭包,导致回调函数所影响的整个组件的内存都无法被浏览器回收。
- 未及时销毁定时器,导致定时器的回调函数影响的内存无法被回收。
- 在销毁业务组件时,未及时销毁引入的第三方组件,会导致第三方组件的dom和事件占据的内存无法被回收。
- vue-router使用不当,可能导致内存溢出。
- 不需要修改属性值,而会修改整体值的引用类型变量,建议使用Object.freeze冻结一下,这样vue处理过程中就不会给引用类型的属性加上getter和setter的监听,可以节省不少内存空间和浏览器性能。
- 虽然道理上来说,只要合理使用变量,那就不应该还需要手动将变量设置为null来提醒浏览器进行回收,但如果实在没有办法了,也不妨试试每一次beforeDestroy时,手动将当前组件实例data中的值设置为null,看看能否达到解决溢出的效果。
4. 由于浏览器或vue框架本身导致的内存溢出
浏览器对SPA框架的内存回收一直存在很多问题,网上查看chrome的官方问题回答,似乎在94版本解决了SPA中的input元素内存无法被回收的问题,但仍然存在select元素和各种事件无法被回收的问题,也不知道后续版本是否解决了这些问题。
vue框架本身也存在很多内存方面的问题,因此你的项目所面临的内存溢出问题,兴许在某些版本中已被解决或不存在,这都是有可能的。