问题引入
考完试在寝室摆烂,刷题之余就顺便点开b站刷刷首页的沙雕视频,几天下来,我的电脑便出现了周期性的卡死和内存泄漏。开始以为是M1pro芯片下macOS的内存泄漏,于是采用注销重新登录来解决。直到某一次,我偶然发现只要关闭全部B站相关的页面,内存压力就会瞬间释放,这时,我才意识到,是叔叔给js里面下过毒,于是便把推文发给一个在b站工作的朋友吐了个槽。

寻找问题
吐槽之后,大佬表示希望收到更详细的问题信息,于是...
回忆了一下自己的经历,简单debug了一下发现,好像是首页的「换一换」按钮导致的。
每次点击会刷新推荐视频,但是,原来的DOM节点并没有被回收,而且莫名其妙导致了Chrome的GC失效。
如图我连续点击多次换刷新首页推荐的按钮,已经导致DOM节点数量暴增,内存开销也从最开始的100M到了1.6G


从上图简单看来,点击「换一换」按钮确实导致了JS堆栈逐渐上升,并且没有梯度下降的迹象,内存中的DOM节点数也呈现相同的规律。
复现过程:
- 打开b站首页(Chrome Version 96.0.4664.110 (Official Build) (arm64))
- 鼠标不移入window(避免触发某些监听事件)
- 等待20s,保证当前页面加载完成,此时js会相对稳定
- 手动进行一次GC,然后dump内存
- 点击「换一换按钮」,等待首页推荐刷新全部完成
- 再次手动GC,dump内存
具体数据如下,每次「换一换」会导致:
- JS堆栈大小永久性增大1M不到
- DOM节点数增加1k左右
- 整个标签页在Chrome任务管理器下增大10M左右
那么有两种可能:
- 每次「换一换」会导致某个数组或对象增大,比如刷新后的视频都被直接push到了队尾,而没有清空原来的队列。
- 「换一换」在刷新首页推荐时,重新分配了对象,但是原来的对象依然存在引用,导致无法被GC进而产生内存泄漏。
尝试分析
通过多次执行上述过程,得到了一组内存dump的数据,每组数据之间都仅执行了点击「换一换」和手动GC的操作。从图可以看出,也证实了上述的规律。

通过和大佬的简单交流,得出了如下的结论
- 最大的第一个array存了很多api获取的data,每次估计是push进去的导致积累
- 第四个最大的闭包closure全是sentry的error wrapper
- 直觉告诉我前几个最大的包括了sentry的error上报数据、闭包,api获取的数据每次没有清空导致积累
深入问题
前面的分析确实很有说服力,是单纯的没有清空数据而导致的数据积累,进而产生递增的内存占用。
但是,为什么每次都会增加1k左右无法被回收的DOM节点呢?
于是我尝试从内存dump中查找每次新增「已分离」的元素,并且每次和前一次进行对比,一组奇怪的数据便引起了我的注意:




显然,这就是首页推荐视频的相关数据,那么问题来了:
- 如果说api获取的数据都是直接push,理论上这个数组应该越来越多,可是我多次点击「换一换」数组里得到的依旧是8个推荐视频。
另外,内存dump每次都和上一次比较,都能发现内容相同的数组。也就是说,在刷新首页推荐后,原来的数据没有被销毁,依然在某个地方保留着引用,进而导致了这些已经分离的DOM节点无法被GC。
进一步查看引用这两个数组的对象,又有了新的发现:


引用了这两个数组的是一个叫「blInlinePlayers」的元素,在首页中检查,它正是新版首页video-card中「鼠标悬停预览播放视频」这个feature使用的播放器。
可是,这个播放器却直接被全局对象「window」所引用,我猜测问题大概就出现在这里。
总结
DOM节点每次增加:有一种可能是因为在初始化播放器后,直接将它挂在到了window中,造成了全局变量污染。在点击「换一换」刷新推荐时,没有调用销毁方法将初始化后的那些播放器销毁,而是直接更新了v-dom上的数据;由于一直保留着引用,进而导致了这些「已分离」的DOM节点无法被GC。
JS Heap中每次数据增加(尤其是JS Array和Typed Array):sentry的error上报数据、闭包,api获取的数据每次没有清空导致积累。
Comments | 0 条评论