视频页面内存泄露的追查与解决方案(记一次 Chrome GC Bug 的发现)
Chrome 通常会对未引用的 DOM 对象定期进行 GC 操作。但是在一定场景下,对 video
的 GC 操作会失效,已经创建的元素,及时已经没有对它引用的指针,依旧会常驻内存中,由此,在对 video
元素不断的销毁和创建过程中,会导致页面内存越来越大,
一 页面崩溃现象
某线上系统页面(标配为Chrome),需要提供大量视频内容信息,但是随着视频列表的翻页,页面内存占用会逐渐升高,直至页面崩溃,运营反馈较多。
二 追查页面崩溃原因
1. 调试环境
压测页面:一个具有 20 个视频条目的列表页面,有翻页按钮。
压测宿主浏览器:chrome(主要),firefox(次要)。
2. 批量整体实验
批量整体实验的测试结果由 chrome 最新版 75 版本测试得出,在页面连续进行 100 次翻页后进行垃圾回收
操作方法:搭建系统页面,使用脚本轮询翻页按钮状态,不间断的模拟按钮点击翻页动作
CPU占用情况
可以看到,CPU占用率一直处在近乎100%的水平上下波动,CPU 达到极限后,页面仍旧可以正常工作,不是引发崩溃的原因
#### 内存占用情况
内存占用分两部分考量
js 堆栈内存:包含 js 执行时上下文中的各种变量对象,这部分是由 V8 来进行垃圾回收
宿主内存:这部分内存体现 dom 节点等宿主浏览器元素的体积,由浏览器进行回收,计算方法:占用总内存 - js 堆栈内存。
通过两种方式来查看这两种内存状态:
打开任务管理器,对比按钮提交脚本执行前后,可以看到,
连续翻页测试执行前:系统内存占用了 274MB,其中 js 内存占用了154MB,宿主内存有 120MB
连续剧翻页测试执行后:系统内存占用了 1.1G,其中 js 内存占用了227MB,宿主内存有 870MB**
对比可以发现:页面翻页过程运行一定次数之后,js 内存提升73MB,宿主内存增加 826 MB
打开 performance monitor 查看状态:
连续翻页测试执行前
连续翻页测试执行后
可以看到,js heap 大小提升 71 MB,DOM节点数由1万提升到 21 万,翻了 20 倍。与任务管理器表现基本一致。
得出初步结论:页面性能较差,CPU占用高(应低优解决),存在较轻的 JS HEAP 内存泄漏(应低优解决),存在严重的 dom 节点泄漏(应高优解决)。
3.单步跟踪实验
根据前面发现的关键点,采用堆快照的方法进行更细致的分析。
a. 堆快照相关概念介绍
detached element
游离的 dom 节点:已经从页面 dom 树移除掉,但是还存在于宿主内存中的 dom 节点
distance
当前节点到根节点的距离,如果某节点没有 distance,通常说明该节点即将被 gc 回收。
shallow size
当前节点自身的内存体积,不包含它所引用的其他对象的体积。
retained size
包含当前节点自身体积和所引用的其他对象体积,表示若该节点被回收,能够释放的总内存大小。
b.实验思路
进入页面后进行 snapshot 堆快照拍摄,翻页后再次进行 snapshot 堆快照拍摄,对比分析快照结果。
c. 页面实验分析
步骤一:
初次进入页面时,进行 snapshot,查看游离的 dom 节点,图中存在 distance 的节点,代表 js heap 中依然有活动对象对它引用,导致它不能释放。:
逐条点击 Detached HTMLDivElement,查看详情:
看到其中部分游离 div 节点是 watermark.js 生成,依然存在对节点的引用阻止了 gc ,我们需要 check 一下这个库是否有泄漏。
步骤二:
点击翻页按钮后,进行 snap shot,查看两次快照对比:
发现 detached element 的整体数量有一个爆炸性的提升,需要对这部分新增的游离节点进行分析。
查看第二次 snapshot 的堆详情:
如上图,我们发现很多游离节点( detached elements) 没有 distance,这代表 js heap 中已经没有活动对象对该游离节点进行引用了,那么该节点应该是可以被宿主回收的。
疑问:是不是刚才进行 snapshot 时系统尚未来得及回收?
验证:多次手动触发垃圾回收,再次进行 snapshot ,发现这些没有 distance 的游离节点依旧存在,存在未知因素阻碍了这些游离节点的正常回收,可以判断页面的 dom 泄漏必然跟这些不能回收的游离节点有关。
针对这部分不能被回收的 detached element 进行重点分析:
1. 首先查看 div element 的泄漏
单独查看堆快照中,不能释放的 Detached HTMLDivElement ,可以看到如下情况
上图中的 DIV 节点被 console 信息中的变量引用导致不能得到释放,我们切到 console 控制台查看输出:
发现大量由于数据格式错误引发的 vue 模版 render 错误栈输出,找到源码位置:
禁用 console.log 一定程度的解决 div dom 泄漏问题。
2. 然后查看 video element 的泄漏
snapshot summary 面板查看 Detached HTMLVideoElement :
发现 pending activities 列表中有对 video 的引用 (pending activities 用于存放未完成的浏览器调用过程,如网络请求等)
查看 pending activities内容:
发现 pending activities 中存放大量 ResizeObserver 任务,这些 ResizeObserver 任务大部分为 video shadowdom 中 div 中所注册,推测这些 ResizeObserver 任务没有得到及时的销毁,导致 video 节点不能释放
初步结论: chrome 对 video 做 gc 时存在 bug 。
Chrome Bug 验证:为了排除其他页面元素的干扰,编写 bug demo https://emyvd.codesandbox.io/ 进行验证,页面会简单重复进行 video 的新建与销毁,这个过程中内存占用和 dom 节点数量持续增加,直到浏览器崩溃,有严重的内存溢出现象,由此该 bug 得到证实。
另外,还对其他 chrome 版本和 windows / mac 下的其他浏览器进行了验证,整理结果如下:
chrome 74 版本首次出现该 bug ,发布时间为四月末,与页面崩溃反馈陡增的时间相符。
还有一个额外的发现:观察点击翻页按钮后的snapshot:
系统页面中只有 20 个 video element ,点击一次翻页会销毁原来的 video element,生成另外的 20 个 video,但是 detached elements 中却存在 60 个 video element,据此推测页面点击提交按钮时,系统进行了两次页面渲染。
验证:已经与业务同学确认,提交时会导致额外的一次视频列表渲染,额外的渲染加重了该 Chrome Video 泄露的影响。
d. 补充验证
进行 allocation timeline 录制,可以验证如上的问题确实是因为点击翻页按钮时刷新页面引起的。
开始录制后,进行提交操作,结果如下:
三 发现问题整理
问题1
因 console log 输出了堆栈信息引用导致的 dom 泄漏问题
解决方案
扫描源码中所有 console log ,控制只输出文本,不允许输出堆栈信息。
问题2
chrome 75 对于 video 元素的 gc 存在 bug
解决方案
方案1:
更换火狐浏览器 (跨平台性)
方案2:
封装自定义 video 组件,内部设有 video dom 资源池以实现对 video dom 的复用: video dom 被 js 从页面中移除时,节点不销毁,而是重新放回 video dom 资源池中,用于新 video 的复用。简单 demo 已经验证通过。
四 修复效果
根据上述方案完成页面调整。
- 测试线上页面提交1500次无崩溃产生
- 原生视频组件的页面 500 次翻页操作实验数据对比如下