普通视图

发现新文章,点击刷新页面。
昨天以前首页

一次痛苦的内存泄露排查经历

2025年5月19日 20:36

前言

这阵子接手的一个需求就是排查项目是否存在内存泄露问题,找到并解决它

目前是发现了四处问题,一处是 el-select 组件存在泄露问题,这个有做出挣扎解决,但是最终并没有彻底解决,无果;一处是项目封装的 v-delegate 指令存在闭包问题,这个最后是组长帮忙发现的,问题很隐蔽;另一个是有个 timer 没有 clear。最后一个还是闭包,这个就是本篇要讲的,排查过程比较煎熬

前端排查内存泄漏还是非常痛苦的,尤其是面对复杂项目

组里的项目是 黑盒语音 客户端,就是大家熟知的小黑盒旗下的一款语音产品,语音项目本身就对内存占用要求比较苛刻

其实排查内存泄漏,定位到是哪些交互其实还好,痛苦的是找到了交互过后,如何定位到具体代码,这次就是一个异步 + 闭包导致的内存泄露问题,开篇前先介绍下如何利用 performancememory 选项卡定位可疑的交互

如何排查(先 performancememory

该项目是 electron + vue2

比如这次碰到的问题是,语聊房的房间设置页面存在多个 tab,我针对所有 tab 依次往下点击然后回到最初的第一个 tab 看 performance 是否存在内存上涨

这里最好记得先把所有的 tab 点击加载一遍,防止有动态加载的组件存在,或者是缓存,异步函数等等。总之最好第一次先点击加载消除某些不可控的影响

这里提一嘴,排查内存泄漏最好是打包之后,或者保证自己的项目不存在 console.log(感觉手动剔除也不现实) ,因为你开启了 console 台后,log 会保存你的变量,这些变量会留在 windows 中导致内存泄露,打包之后一般 tree-shaking 会帮我们将项目的 log 给自动删除

好,现在我们来利用 performance 进行排查

从 第一个 tab 开始切换下面的所有 tab,然后回到第一个 tab

1.gif

在录制之前保证点击了所有的 tab 后,开始录制时记得勾选 memory ,这个就是我们要看的 内存 信息,交互前先点击 🧹 图标,最后回到第一个 tab 后结束录制前也记得 点击 🧹 图标

最后我们来注意整个交互区间的 Nodes 上下浮动范围,从图中可以看出,节点从最初的 7000 个增长到了 8000 个左右,明显有内存泄露问题

这也就意味着这么多 tab ,存在一个或者多个导致了内存泄漏,正常来讲我回到页面最初起点,页面的 dom 数量也只会是最初的,增长了也就意味着 dom 可能被某些数据引用了,成了游离 dom,前端内存泄漏最大的问题其实就是游离 dom

接下来的分析方向就比较清晰了,我们需要排查究竟是哪个 tab 导致了内存泄露,后面的定位会比较繁琐,因为你要挨个排查,挨个排查你就得控制好变量,比如我怀疑第一个 tab,那么我就需要将其余 tab 的组件代码的 代码(template + script) 清空,然后针对第一个 tab 来回切换

中间的步骤这里不会展示,这里直接说结论了,就是第一个 tab 有问题,为了再次证明是这个 tab 的问题,我们接下来可以利用 memory 选项卡进行内存快照分析

为了有一个 tab 可以辅助切换,但是又不能有这个 tab 的影响,我就需要将其 template + Script 部分代码置空,比如这里我将 第二个 tab 代码置空

我们现在进行 memory 分析

2.gif

可以看到第一个tab在与第二个空tab来回频繁切换 10 次后,js heap 上涨了将近 20M,这还是非常恐怖的数据

这个时候我们就可以去定位到是第一个 tab 这个组件的问题所在了,当然我们其实还是可以去继续留意 memory 的变化

1.png

我们会发现有两个 很奇怪的 constructor 增长了,一个 t 一个 a,我将这个项目放到 web 上去观察反而没有这两个变量,我们随机展开一个 t 看内部结构

2.png

会发现这应该是 vnode,这应该是因为 electron 跑的是代码压缩后的结果,还有个 a 应该是 VueComponent

其实内存快照的 comparison 这里也只有 t 好去分析,也就是 vnode,因为一个 vnode 会对应一个 dom 节点,我们可以看看究竟是哪里多出的 游离节点

其实当我们定位到某个组件的时候,我们还需要进一步分析,因为有些组件可能是由多个封装组件进一步封装的,或者会有多个同级 div,这个时候我们又需要去做一个控制变量分析,依次保留当前组件的某部分 template 然后去拍 内存快照,有时候可能是 js 问题,那又要控制 js 代码。

因为第一个 tab 组件比较复杂,所以这一步废了挺多时间去排查某个具体部分

其实定位到组件的时候,我们可以先目测观察下当前组件是否存在一些没有 off 掉的事件或者没有 clear 的定时器,然而事实却是 on 的事件都有对应的 off ,定时器也都有 clear,这就加大了排查难度

最后是定位到了一个 异步 methods,这个 methods 大致如下

3.png

其实这个 initData 中间还有很多逻辑,这里只展示了重点。乍这么一看好像也没啥问题,最后我定位的过程中,发现就是 judge 有问题,当我在 judge 中直接 return true 时没问题,只要一引用了 vuex 的值就会有泄露

这里的 channel_list 就是一个 vuexstore 值,我若是切换切得很快,这个 异步函数 在组件卸载时可能还没有执行完毕,后面的 initChannels 就会排队执行,这个 initChannels 里面的 judge 又是个 闭包函数,并且通过 vuex 引用了 thisvuex 本身就是全局唯一的状态管理库,这个值若牵扯到了 this,也就是 vue 实例,就会引起内存泄漏问题

所以怎么解决这个问题呢,我们可以在 await 后添加一个 逻辑,若组件卸载了就直接 return,不让继续执行后面的逻辑

4.png

这个泄露 bug 排查最后还是组长点醒我的

其实后面排查的过程中,因为用了组件库 element-ui,其中还有个 tab 用到了 el-select 组件,这个组件也存在内存泄露问题,好像 element-ui 但凡涉及到 popover 的组件都存在内存泄露问题,大家使用这个库的时候还是谨慎点

最后

当我们使用 performance 或者 memory 选项卡定位到了某个组件存在内存泄漏问题时,首先应该去判断组件是否存在某些事件没有清除,或者定时器没有 clear,这个是最重要的,若肉眼难以看出来,那就进一步去怀疑是否存在闭包导致的内存泄漏,然后去通过注释代码的方式去验证想法,过程还是非常麻烦的

❌
❌