三行代码,修复 DOM 内存泄露

7,384 阅读5分钟

前言

彦祖们,内存泄露应该是前端老生常谈的话题了

但是我们往往停留在理论层面,应付面试官,却十分缺少实战机会

本文是笔者在业务开发中遇到的一次真实案例,以此作为分享

技术栈:vue2(业务限制,未使用 keep-alive)

先看下 vue 官方文档

v2.cn.vuejs.org/v2/cookbook…

image.png

场景

业务场景非常简单,就是两个 tab 页面互相切换,但是业务终端不是 pc

而是我们工业互联网业务中的工控机

因为可执行内存只有 1g,所以会经常出现卡顿的情况

彦祖们可以想象成那种医院挂号的终端

场景复现

操作流程

彦祖们,看下我此时的界面(主要关注下 DOM节点)

  1. 查看站点组情况

此时是 846

image.png

  1. 点击 单设备界面(单机界面)

发现此时的 DOM节点 增加到了 1587

image.png

当然这是正常现象

多出来的节点个数不就是我这个 单机界面 渲染的节点个数吗?

  1. 回到 站点组界面

惊人的发现 DOM 节点个数飙升到了 2862

image.png

  1. 不断来回切换

在笔者来回切换了五六次之后,此时节点个数来到了 5477

image.png

排查问题

猜想

我们初步猜想是 单机界面 存在节点内存泄露,那么该如何来证明这个猜想呢?

DevTools 介绍

我们需要借助 chrome devtools 来排查这个问题

  1. 打开 devtools 切到 内存 tab (英文版自行翻译)

image.png 2 . 选取 时间轴上的分配插桩 image.png

3 . 点击回收垃圾(每次录制前需要手动回收,以保证数据准确) image.png

  1. 在我们的 单机界面 点击开始录制

image.png

  1. 点击结束录制,我们能看到时间桩上会分布 蓝色/灰色 的柱状图

image.png

image.png

柱状图的分布展示了新内存分配时机

蓝色部分表示目前还占用的内存

灰色部分表示目前已被回收的内存

此案例中我们主要关心的是内存分布区域的构造函数

我们检测下有没有 Vue组件泄露

开始录制时间桩

下面我们来模拟下业务中的场景

也就是站点组 单机界面来回切换,生成内存分布图

2024-05-26 13.44.30.gif

分析时间桩

  1. 我们利用左右两边的框选工具,定位到第一次切换到单机界面的柱状图

image.png

此时我已经切到了 站点组,但是蓝色区域还有大部分没有被回收,让我们来看看是否有单机界面的内存泄露

image.png

  1. 过滤出 Vue

image.png

一个个检查下,是否有哪个组件只存在单机界面的但是没有被回收

这个 .alter-message 笔者全局搜了一下

并确保只有在 单机界面 使用

前文已经提到,业务中未使用 keep-alive 进行缓存

那么 vue 应该帮我们销毁这个组件了呀

  1. 查看组件详情

我们来深入看下这个组件的信息,发现 vue 的确已经 destroy

image.png

这就很奇怪了,难道是所谓的 游离节点?

不要慌,我们点击组件下面会展示内存分布详情

image.png 有些可能无法定位到代码文件,比如这种

image.png

这时候笔者就把 26 个泄露的文件一个个检查了一遍...(如果有更好的方式,欢迎评论区留言)

  1. 查看代码

那么我们就进去看看代码,一探究竟,发现以下三处可疑的地方

image.png

image.png

image.png

一步步定位到这两段代码,彦祖们应该已经发现问题所在了

插入一段八股文

此时不禁想起了这段八股文

面试官:造成内存泄露的情况有哪些?

彦祖: 未正确使用闭包 | 事件监听器未移除 | 定时器忘记清理 | 使用插件时,未销毁 | 意外的使用全局变量未回收 | console.log 未清除...

回到正题

没错,我们这里就是命中了三个场景

  1. 事件监听器未移除
  2. 定时器忘记清理
  3. 使用 echarts 时,未销毁

导致堆内存不知道你什么时候会销毁这个事件,这个定时器有没有用它也不知道啊~

这样一来也就长期劫持了对 vue组件 的引用

也就是说每次进入单机界面 都需要开辟新的内存以支持组件的渲染

看下这张图

image.png

如果一个界面中某个组件未被完全移除

那么整个界面的所有组件所占用的内存都不能被完全释放

验证猜想

其实以上种种都是我们的猜想

接下来我们加上以下三行代码

  1. 移除全局事件监听
window.removeEventListener('closeException',this.xxx)
  1. 移除全局定时器
clearInterval(this.timeInterval)
  1. 销毁 echart 实例
this.myChart.dispose()

再来抓一下内存分布

image.png

会发现已经没有泄露的 vue component

并且 DOM 节点 会被回收到 870 不会重复上升

改动3行代码,完美收工,走一个 998 🥳

坑点

彦祖们,补充一个坑点,在调试过程中,最好把 console 全去掉

笔者就在调试过程中遇到了一个坑,发现了这行代码

是的,打印了一下 vue 实例

console.log('实例', this)

结果导致内存怎么都回收不了

给问题定位带来了重大的干扰

不过这也完美解释了为什么 console.log 为什么会导致内存泄露

总结

总结一下,如果彦祖们怀疑 A 页面有内存泄露,那么测试步骤如下

1.打开内存监视

2.A B A B 来回切换

3.关键!!!最后停留在 B 页面(不然 A 页面渲染了就会导致无法回收),停止内存监视

写在最后

本次优化仅仅修改了3行代码

最复杂的问题,往往伴随着最简单的代码改动

但是问题的定位过程,是非常考验彦祖们的能力的

感谢彦祖们的阅读

个人能力有限

如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟