一次痛苦的内存泄露排查经历
前言
这阵子接手的一个需求就是排查项目是否存在内存泄露问题,找到并解决它
目前是发现了四处问题,一处是 el-select
组件存在泄露问题,这个有做出挣扎解决,但是最终并没有彻底解决,无果;一初是项目封装的 v-delegate
指令存在闭包问题,这个最后是组长帮忙发现的,问题很隐蔽;另一个是有个 timer 没有 clear。最后一个还是闭包,这个就是本篇要讲的,排查过程比较煎熬
前端排查内存泄漏还是非常痛苦的,尤其是面对复杂项目
组里的项目是 黑盒语音 客户端,就是大家熟知的小黑盒旗下的一款语音产品,语音项目本身就对内存占用要求比较苛刻
其实排查内存泄漏,定位到是哪些交互其实还好,痛苦的是找到了交互过后,如何定位到具体代码,这次就是一个异步 + 闭包导致的内存泄露问题,开篇前先介绍下如何利用 performance
和 memory
选项卡定位可疑的交互
如何排查(先 performance
后 memory
)
该项目是 electron + vue2
比如这次碰到的问题是,语聊房的房间设置页面存在多个 tab,我针对所有 tab 依次往下点击然后回到最初的第一个 tab 看 performance
是否存在内存上涨
这里最好记得先把所有的 tab 点击加载一遍,防止有动态加载的组件存在,或者是缓存,异步函数等等。总之最好第一次先点击加载消除某些不可控的影响
这里提一嘴,排查内存泄漏最好是打包之后,或者保证自己的项目不存在 console.log
(感觉手动剔除也不现实) ,因为你开启了 console 台后,log 会保存你的变量,这些变量会留在 windows
中导致内存泄露,打包之后一般 tree-shaking
会帮我们将项目的 log 给自动删除
好,现在我们来利用 performance
进行排查
从 第一个 tab 开始切换下面的所有 tab,然后回到第一个 tab
在录制之前保证点击了所有的 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
分析
可以看到第一个tab在与第二个空tab来回频繁切换 10 次后,js heap 上涨了将近 20M,这还是非常恐怖的数据
这个时候我们就可以去定位到是第一个 tab 这个组件的问题所在了,当然我们其实还是可以去继续留意 memory
的变化
我们会发现有两个 很奇怪的 constructor
增长了,一个 t
一个 a
,我将这个项目放到 web 上去观察反而没有这两个变量,我们随机展开一个 t
看内部结构
会发现这应该是 vnode
,这应该是因为 electron 跑的是代码压缩后的结果,还有个 a
应该是 VueComponent
其实内存快照的 comparison
这里也只有 t
好去分析,也就是 vnode
,因为一个 vnode
会对应一个 dom
节点,我们可以看看究竟是哪里多出的 游离节点
其实当我们定位到某个组件的时候,我们还需要进一步分析,因为有些组件可能是由多个封装组件进一步封装的,或者会有多个同级 div,这个时候我们又需要去做一个控制变量分析,依次保留当前组件的某部分 template
然后去拍 内存快照,有时候可能是 js 问题,那又要控制 js 代码。
因为第一个 tab 组件比较复杂,所以这一步废了挺多时间去排查某个具体部分
其实定位到组件的时候,我们可以先目测观察下当前组件是否存在一些没有 off
掉的事件或者没有 clear
的定时器,然而事实却是 on
的事件都有对应的 off
,定时器也都有 clear
,这就加大了排查难度
最后是定位到了一个 异步 methods
,这个 methods
大致如下
其实这个 initData
中间还有很多逻辑,这里只展示了重点。乍这么一看好像也没啥问题,最后我定位的过程中,发现就是 judge
有问题,当我在 judge
中直接 return true
时没问题,只要一引用了 vuex
的值就会有泄露
这里的 channel_list
就是一个 vuex
的 store
值,我若是切换切得很快,这个 异步函数 在组件卸载时可能还没有执行完毕,后面的 initChannels
就会排队执行,这个 initChannels
里面的 judge
又是个 闭包函数,并且通过 vuex
引用了 this
,vuex
本身就是全局唯一的状态管理库,这个值若牵扯到了 this
,也就是 vue
实例,就会引起内存泄漏问题
所以怎么解决这个问题呢,我们可以在 await
后添加一个 逻辑,若组件卸载了就直接 return
,不让继续执行后面的逻辑
这个泄露 bug
排查最后还是组长点醒我的
其实后面排查的过程中,因为用了组件库 element-ui,其中还有个 tab 用到了 el-select
组件,这个组件也存在内存泄露问题,好像 element-ui 但凡涉及到 popover 的组件都存在内存泄露问题,大家使用这个库的时候还是谨慎点
最后
当我们使用 performance
或者 memory
选项卡定位到了某个组件存在内存泄漏问题时,首先应该去判断组件是否存在某些事件没有清除,或者定时器没有 clear
,这个是最重要的,若肉眼难以看出来,那就进一步去怀疑是否存在闭包导致的内存泄漏,然后去通过注释代码的方式去验证想法,过程还是非常麻烦的