最近在项目开发时,遇到了一个内存泄漏的问题,用了一些时间排除,特此记录。
内存泄漏问题
在vue项目数据处理页面数据展示区域,有一个联动功能,选择前面的select的内容后,后面选择框的内容会根据前面的数据去获取,出现的问题是每次选择前面的选择框后,页面操作会变得卡顿, 频繁进行select操作后页面崩溃。
打开chrome调试工具,选中memory选项,发现每次select操作后,内存都急剧增大,且通过gc强制回收后内存没有减少,可以确定是出现内存泄漏了。
Chrome 内存分析方法
内存分析主要使用Chrome的debug工具Profiles面板,Profiles可以追踪网页程序的内存问题。打开控制台,选择Memory选项, 如图,左侧是Profiles的start等操作选项,右侧是Profiles提供的功能选项
Profiles提供了3个功能项
Heap snapshot
通过创建堆快照可以查看创建快照时网页上的JS对象和DOM节点的内存分布情况,使用该工具可以创建JS的堆快照、内存分析图、对比堆快照帮助定位内存泄漏问题。
Allocation instrumentation on timeline
从整个Heap角度记录内存分配的时间轴信息。
点击Start按钮之后,执行可能会引起内存泄漏的操作,操作之后点击左上角的Stop按钮即可。在蓝色竖线上通过缩放过滤构造器窗格来显示在指定的时间帧内被分配的对象信息。在录制过程中,在时间线上会出现一些蓝色竖条,这些蓝色竖条代表一个新的内存分配。
Allocation sampling
用于分析网页上的JS函数在执行过程中的CPU消耗信息。
点击Start按钮,执行你想要去深入分析的页面操作,当你完成你的操作后点击Stop按钮。然后会显示一个按JS函数进行内存分配的分解图,默认的视图是Heavy,该视图会把最消耗内存的函数显示在最顶端。
问题复现
将数据展示区域select框部分抽离为demo,减少干扰,便于分析。
<template>
<div>
<table>
<tbody>
<tr v-for="(data,index_data) in tableData" :key="index_data">
<td>
<el-select v-model="data.moduleName" @change="selTargetChange()" placeholder="请选择">
<el-option v-for="item in moduleList" :label="item" :key="item" :value="item"> </el-option>
</el-select>
</td>
<td>
<el-select v-model="data.target" default-first-option filterable placeholder="请选择">
<el-option
v-for="item in tableTargetList"
:key="item"
:value="item">
</el-option>
</el-select>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
data(){
return {
flag:false,
tableData:[
{
target:"target",
moduleName:"moduleName"
},
...
],
tableTargetData:[...], //大量数据
tableTargetList:[],
moduleList:[...]
}
},
methods:{
selTargetChange(){
this.flag = !this.flag;
this.tableTargetList = this.tableTargetData
if(this.flag){
this.tableTargetList = null;
}
},
},
}
</script>
问题分析
通过Chrome提供的堆快照进行分析,进入页面时,选择Chrome的Heap snapshot,点击start对页面进行一次快照,然后点击select选择内容后再对页面进行一次快照,通过Comparison对两次快照进行对比。可以看到增加了大量的数组,闭包,对象等,占用了大量的内存。
展开数组,发现增加了大量VueComponent,每个VueComponent都有大量的数据与之关联。
通过代码调试分析知道select框中的每一条数据最终都会以一个vue组件的形式展现,有多少条数据就会渲染多少个vue组件。
只点击一个select框,所有的select框都渲染了。因为vue核心是数据驱动视图,通过分析代码发现所有的select框都是使用同一个data数组渲染的,导致每次select选择后data数组改变,所有的select框都会重新渲染,当select框中的选项特别多时,这种开销是非常巨大的。
但是这只是定位了内存飙升的问题,每次点击后内存大小应该会根据select框中选项数据量变化,而不是一直增加。
在demo中通过代码将第二次点击后select选项值置为空,通过两次快照比较内存变化。
对比发现两次操作后增加了很多数组,对象和虚拟dom节点,其中最重要是Detached HTMLLIElement这一项,展开Detached HTMLlLLIElemen查看。
发现有许多游离的dom节点。这些dom节点关联了大量的数据,导致内存没有释放。鼠标悬浮于dom节点几秒后会弹出节点的信息。
发现每一个游离的dom节点都是select框中option选项中的一个组件,可以得出结论即数据改变后,dom对象没有被销毁。
展开dom节点的信息,在下面的方法绑定中发现element编译后js文件存在对该dom节点的引用导致该dom没有被释放。
解决方案
至此,已经分析出了内存泄漏的原因,针对以上两个问题导致的内存飙升和内存泄漏问题使用如下方案解决。
1,为每个select框设置单独的渲染数组,这样每次点击后就只会渲染对应select框,解决内存飙升的问题。
2,因为内存泄漏的问题是使用element-ui框架的select组件造成的,可使用原生的select框替代。
总结
在前端开发中,出现内存泄漏问题时可以通过Chrome分析工具帮助我们快速定位问题,根据问题制定解决方案。
但是前端开发中很少会注意到内存泄漏的问题,有些内存泄漏需在频繁操作后或数据量特别大时才会有明显的表现,在开发时一般很难发现。但一旦部署到线上出现问题后往往可能会造成无法估量的损失。所以在开发中对于有dom操作或数据量较大时的处理,一定要谨慎编码。