【有价值】vue3内存泄露问题定位过程

本文包含以下章节:

  • 1、内存泄露的排查点
  • 2、案例一:树组件的每个节点增加hover事件导致内存泄露
  • 3、案例二:表格组件的slot中使用表格行数据row作为内联函数的参数和动态判断class的依据
  • 4、案例三:表格组件的slot中使用表格行数据row作为参数传递给子组件
  • 5、案例四:树组件的子节点数据懒加载回来后,将数据绑定在当前点击的父节点node上,此处直接修改treeData的一个节点数据,导致旧的表格数据没有释放,出现内存增长从而泄露
  • 6、工具使用:chrome性能监控

写在最后的总结:写完整个经验总结后,发现4个案例有一个共同点:el-table、el-tree等组件的点击事件或插槽slot中如果给你返回了行数据row、列头信息column、节点数据node等参数,千万不要直接修改他们。如果有修改的业务场景,将要修改的数据先深拷贝一份然后修改拷贝的数据,所有的修改,想要响应式的反映在页面上时,一定是整个重置treeData或者tableData,而不能重置他们中的某一个行数据或者一个节点node数据。

// =====正确
// 克隆row
let rowCopy = cloneDeep(row)
// 修改克隆的row
rowCopy.editableField={label: '想修改的值'}
// 克隆tableData
let tableDataCopy = cloneDeep(tableData)
// 修改克隆的tableData
tableDataCopy[index] = rowCopy 
// 重置tableData
tableData.value = [...tableDataCopy ]

// =====错误
row.editableField={label: '想修改的值'}  // 错误:直接修改row
tableData[index] = rowCopy  // 错误:直接修改tableData中的一行数据

一、内存泄露的排查点

1、本次排查出的问题(每一个都对本次泄露问题有影响)

  • 树组件的slot中,不要给每个节点绑定div,可以考虑在父节点上绑定一个div然后每次调整显示位置
  • 树组件的slot中,不要给每个节点绑定事件(如mouseenter、mouseleave、click),可以考虑用样式替代或者在父节点上增加一次事件
  • element-dialog泄露:dialog嵌套虚拟树或者大型数据的组件时,一定要配置destroyOnClose为true,并且配合下面两点使用才有效:在对话框关闭时数据对象一定要置为null、对话框内容区域的组件或dom,一定要用v-if,跟随对话框关闭时销毁内部组件
  • 公共组件虚拟树中,引用超大的国际化文件,导致内存泄露(虚拟树被嵌套在对话框中才会暴露该问题)
  • table、tree组件使用时,v-if的判断中不能有data数据或data.length>0这样的判断
  • template中不要使用箭头函数,比如:<div @click='e => func(e, row.id)'>
  • template中函数的参数,不要使用数据对象,比如:<div @click='e => func(scope.row)'>,因为vue渲染时一直使用该变量,导致dom元素无法释放
  • 样式类尽量提前定义好,不要使用数据作为参数来动态计算样式类,如<div :class='{"aClass": scope.row.isA}'>。因为vue渲染时一直使用该变量,导致dom元素无法释放
  • 闭包导致的泄露(后面补充案例:websocket封装过程中,回调函数未释放导致泄露)

补充概念:内联函数(inline function)在 Vue 模板中指的是直接在模板中定义的函数表达式,它们通常会导致性能问题和潜在的内存泄漏。常见形式包括以下3种:

<!-- 1. 事件处理中的内联函数 -->
<button @click="() => doSomething(item)">按钮</button>

<!-- 2. 插值中的内联函数 -->
<div>{{ item.value.toFixed(2) }}</div>

<!-- 3. 计算属性中的内联逻辑 -->
<div :class="{ active: isActive(item.id) }"></div>

2、常见内存泄露排查点

  • 全局变量在onUnmounted中置为null,完成清理,尤其是大型数据
  • 定时器在onUnmounted中置为null完成清理,如setTimeout和setInterval
  • dom和事件要在onBeforeUnmount中销毁,变量在onUnmounted中销毁
  • 动态添加的dom元素,要及时清理
  • 深度拷贝,用lodash-es的cloneDeep方法代替Json.parse(Json.stringfy(aa))
  • 用WeakMap 替代对象缓存字符串
// 不推荐
const cache = {};
function process(data) {
  cache[data.id] = JSON.stringify(data); // 无限增长
}
// 推荐
const cache = new WeakMap();
function process(obj) {
  const str = JSON.stringify(obj);
  cache.set(obj, str); // 随对象自动回收
}
  • 数组 join 替代字符串拼接
  • 固定对象结构
// 不推荐:创建大量不同形状的对象
function createUser() {
  const obj = {};
  obj[Math.random().toString(36)] = true;
  return obj;
}

// 推荐始终使用相同属性结构
function createUser() {
  return {
    id: null,
    name: null,
    // 明确所有可能字段
  };
}
  • 使用 Map 替代动态对象
// 不推荐:同一变量赋值不同形状对象
let obj;
if (Math.random() > 0.5) {
  obj = { a: 1 };
} else {
  obj = { b: 1, c: 2 }; // 不同shape
}

// 推荐
const obj = new Map();
obj.set('dynamicKey', true); // 不影响对象shape
  • 清除或重置store中的变量

二、问题现象以及定位过程:案例一

1、问题现象:页面是左树右表,树组件使用的是我们自己基于el-tree封装的组件,额外增加的功能是,鼠标hover事件会触发节点的增删改按钮浮动框出现,用来增删改当前树节点;同时,点击树节点后,右侧表格内容会更新。问题现象是,每次点击树节点,会发现内存增长过快

界面截图
左侧树放大

2、技术使用:vue3+element-plus

3、问题定位思路和过程:

1、排查点击事件函数是否有dom的处理,没有发现问题
2、排查template中的对事件的写法,发现每个节点都绑定了mouseenter和mouseleave事件,用来显示增删改按钮组。验证后发现,确实是这两个鼠标事件导致内存快速增长。判断依据是,鼠标只需要在树节点上滑动,并不需要点击,就能看到内存快速增长到500M
3、每一层节点上都绑定了该事件,并且使用v-show默认隐藏按钮组div。实际业务上,只需要对最外出节点显示按钮组,其他子节点不需要


每个节点都绑定了mouseenter和mouseleave事件

4、分析:一、不应该给每个节点增加事件,应该替换成在父节点上增加一个事件,然后使用冒泡去处理业务逻辑,减少事件触发;或者采用hover样式替代事件。二、不需要的节点上,不增加额外的事件,只给最外层增加即可;三、用v-if替换v-show,减少不必要的dom渲染。优化后的代码如下所示:

删除事件绑定,使用v-if替换v-show
使用hover样式替代事件

三、问题现象以及定位过程:案例二

1、问题现象:有一个表格,单元格中有一个按钮,点击按钮会弹出对话框。对话框左边是虚拟树,右边是表格

三个点的圆圈是按钮,点击能打开对话框

对话框中的内容

2、技术使用:vue3+element-plus

3、问题定位思路和过程:

1、排查我们自己封装的虚拟树,没有泄露
2、排查我们自己封装的对话框,有泄漏:对话框内嵌套5万个div,关闭时不释放dom,得出结论:我们封装的对话框有泄漏
2、排查element的对话框,有泄漏:对话框内嵌套5万个div,关闭时不释放dom,要配置destroyOnClose,并且5万个div外嵌套一个总的div,总的div上用v-if=dialogVisble在关闭对话框时销毁全部div。得出结论:element的对话框本身有泄漏,需要配置属性,并且嵌套内容要配合v-if;我们自己封装的对话框,也要这样修改
3、我们封装的对话框中嵌套elment原生的虚拟树,有泄漏,需要把虚拟树的treeData对象在关闭对话框时置为空数组或者null。得出结论:element的对话框依然有泄漏,需要把treeData在关闭时置为空数组或者null
4、修改以上问题后,用我们封装的对话框中嵌套我们封装的虚拟树,依然有泄露,二分法删除我们封装的虚拟树组件的代码,最后定位原因是,引用了一个超大的国际化文件得出结论:对话框嵌套我们封装的虚拟树有问题,要删除国际化文件的引用
5、-------------至此,我们封装的对话框嵌套我们封装的虚拟树,已经没有问题。可以回到问题暴露点上了
6、点击表格单元格内的按钮,打开对话框,有泄漏,删除对话框,多个表格切换,仍然有内存泄露,说明表格本身有泄漏。得出结论:表格使用中有泄漏问题
7、二分法删除表格的业务代码,最后发现表格的slot写法中有多处泄露(代码可参考后面的截图)。一个是,div的样式类是动态的,它使用了表格数据来判断是否有该样式类;另一个是,表格的一列单元格的内容是字典,需要根据字典的code查找出对应的label。得出结论:打开对话框出现内存泄露只是表象,在这个问题中,任何添加dom的操作,都会导致dom只增不减,因此切换多个表格也会出现同样的泄露问题,表格出了问题,不是对话框出了问题;更重要的一点是,table组件的slot中,尽量不要使用渲染数据来做判断、也不要作为内联函数的参数

错误一:使用表格数据动态添加样式类
错误二:表格单元格渲染依赖内联函数

8、错误修复
问题一如图,数据中提前计算好样式类,table组件的slot中直接使用,不再计算


错误一修改:数据提前计算样式并保存在数据中
错误一修改:dom中使用数据中提前算好的样式类

问题二如图:表格渲染时,内联函数中不要把渲染的行数据作为参数传递,只传递行数据的下表index,然后行数据可以通过tableData[index]方式获取到


错误二修复:内联函数参数用scope.$index替换原来的scope.row
错误二修复:内联函数中通过index获取行数据rowItem

注意:内联函数中,当index为-1时,或者tableData没有数据时,一定要返回,否则依然会泄露,这个点比较隐秘,很难发现

四、案例三:给子组件传参引发的内存泄露

如下图,左侧是有问题的代码,右侧是修改后的。问题仍然是将表格的某一行数据作为参数传递给子组件。无论子组件是否修改了改参数,都会导致dom不释放。右侧是修改后的代码,修改思路依然是,不传递表格的行数据,可以传递表格全部数据以及当前行的下标,子组件根据这两个参数就能获取到当前行的数据。


image.png

五、案例四:树组件的子节点数据懒加载回来后,将数据绑定在当前点击的父节点node上,此处直接修改treeData的一个节点数据,导致旧的表格数据没有释放,出现内存增长从而泄露。

image.png

六、工具使用:chrome性能监控

1、两种方式,第一个比较难用,第二个比较常用

1、chrom的开发者模式中,memory页签下,可以抓取两次快照,对比内存泄露的大小、泄露的对象、泄露的文件和代码。
【点评】亲测不好用,因为vue3组件嵌套过深,依赖的框架和第三方较多,对比出来的维度和条目有十几万条,并且结果中将插件和自己编译后的代码混合在一起,很难找到自己写的文件的泄露点,总体来说,就像大海捞针

2、chrom的devTools中,右上角的三个点标志中,找到more tools中的performance monitor,然后结合memory页签下的回收内存按钮,就可以监控js内存、dom大小和事件监听。js内存和dom持续上涨,一定有泄露,最实用也是最靠谱的办法是,二分法删除代码,不断缩小范围,最后锁定问题。
【点评】性能监控工具,就是一个指南针,给出一个方向和泄露体量的指标值。要解决具体问题,还是得逐行查看自己的代码。这个过程一定会非常耗时,做好准备,踏实使用二分法对代码地毯式排查,没有捷径。因为每一个问题都有可能与之前的不一样,之前积累的经验在关键的时候有用。但是,达到关键点之前,大量的排查工作,将问题锁定到很小的范围之内,是必不可少的,少了排查,关键点不可能到来

2、方式一使用说明

两次快照对比时,字段的意义如下:

  1. alloc.size (Allocated Size)
    定义:在两次快照之间新分配的内存总量

计算方式:sum(所有新创建对象的内存大小)

正常情况:在操作过程中会有合理的新内存分配

异常信号:持续高 alloc.size 且不被释放可能表示泄漏

  1. freed size (Freed Size)
    定义:在两次快照之间被垃圾回收的内存总量

计算方式:sum(所有被销毁对象的内存大小)

健康模式:应该与 alloc.size 保持相对平衡

危险信号:长期低 freed size 表明对象未被正确释放

  1. size delta (Size Delta)
    定义:内存净变化量

计算公式:size delta = alloc.size - freed.size

解读:

0:内存净增长(潜在泄漏)

≈0:内存平衡(健康状态)

<0:内存释放(可能是缓存清理)

内存泄漏判断标准-确定泄漏的强信号:

连续多次快照对比都显示 size delta > 0

freed size ≈ 0 而 alloc.size 持续增长

特定对象类型的 delta 持续为正

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容