记一次播放器页面白屏时间优化方案

866 阅读7分钟

优化对象

此次优化是针对,Web 端视频播放页面。白屏时间按照播放视频状态有两种情况,分别是:

  • 外部进入,包括筛选页进入、分享链接进入、用户详情页进入和页面强制刷新,此时白屏时间可以理解为浏览器输入 url 到播放器播放的时间,主要流程分为四部分:
    • 完全白屏,FP(First Paint) 之前流程
    • 初始渲染视图和 loading 出现,sessions 接口请求极其依赖逻辑执行
    • sessions 之后的流程
    • loading 消失,开始播放
  • 内部切换,通过手动点击【用户会话】列表项切换视频,此时白屏时间可以理解为 loading 出现到消失的阶段,有三部分:
    • loading 出现
    • sessions 之后的流程
    • loading 消失,开始播放

整理流程图示如下:

image.png

预期结果

正常情况下,新版本视频目标达到 秒开,老版本视频因为需要手动解析事件,可能会稍慢一些。

已做工作

  1. 事件流分页(加载第一页数据后即开始播放)
  2. 提前渲染,不等待数据准备完毕,第一时间渲染播放器、会话列表和事件流框架,播放前展示 loading ,给用户视觉上更好的体验,「觉得快」
  3. 引入 Web Worker
    1. 新版本视频因为返回数据为字符串,需要手动解析为 JSON,因此下载和解析逻辑移入 Web Worker,充分利用 CPU 资源,避免影响主线程逻辑和渲染,播放前主要是不影响数据流,事件流的解析和渲染。
    2. 老版本视频下载和解析逻辑移入 Web Worker。
  4. 全页面去掉自定义滚动条
  5. 开发者工具 source 面板懒加载

整体分析

借助 Chrome Performance 录制白屏至视频开播之间的片段,进行分析:

因为 FP 用于记录页面第一次绘制像素的时间,之后即会进入动态代码阶段,所以下面以 FP 为界限,分别分析前和后两部分。

FP 之前分析

下面是 FP 之前的详情截图

从图中可以看出耗时的任务主要为两部分:

  • my-details.js 文件下载(目前 gzip 压缩之后,体积为737.25 KB)
  • 一个大 Task

问题定位

  1. 文件下载阶段,打包文件过大,通过 webpack 的打包插件 webpack-bundle-analyzer 分析,如图所示:

经过分析,三个最大的文件是 ts.worker.js(gzip 1010.22 KB) 、my-details.js(gzip 737.25 KiB) 和 chunk-vendor.js(gzip 442.56 KiB),三个大文件中的较大模块如下:

image.png 其中 monaco-editor 是最大的一个问题点。

  1. js 文件执行阶段,具体分析下大 Task,Performance 放大截图如下

分析 Task 执行顺序,执行流程图如下(只列出主要流程,打包之后执行流程会不同,但不影响分析):

image.png

结合 Performance 结果,可以看出其中 monaco-editor 仍然是最大的问题点。

进一步分析问题如下:

  • chunk-vendor.js 是项目所有的依赖库,从它是打包了node_modules可以看出,所以很影响性能
  • monaco-editor 编辑器占了很大体积
  • 对于一些其他的工具库,未采用按需引入的方式

优化方案

优化 FP 指标,关键是缩短资源的下载时间,以及减少阻塞浏览器渲染 DOM 的任务的执行时间,通常来说的手段有骨架图、分块加载、缓存、减少同步js代码、资源压缩和资源按需加载等等。对我们来说大部分工作已经做了,现在关键点为有几个文件很大。

因此,总结下优化方案:

  1. 拆分过大 js 文件,尤其是 monaco-editor 的抽离,因为用的地方太多了, 导致很难做成按需加载了的了,可以使用splitChunks分离代码并实现相关模块共享。单独打包monaco-editor,最终减少请求资源的大小和请求次数
  2. 替换过大的第三方类库,主要是 lodash.js 和 moment.js
  3. 工具库采用按需引入的方式,尤其是 element-ui
  4. 缓存部分静态资源,提高加载速度

补充文档webpack 性能调优报告

FP 之后分析

问题定位

FP 之后就进入了代码执行阶段,下面为 8 个会话按照几个重要节点进行耗时统计的柱状图:

由柱状图可以看出,vue 数据处理和 dom 挂载(mounted之前)占用了很小的范围,几乎对白屏没有任何影响。重点关注两个阶段

  • mounted -> playInit ready,即播放初始数据(playInit)准备好之前
  • playInit ready -> play,即拿到 playInit 到正常播放

这两个阶段加一起的耗时平均接近 2 秒,极端情况可能达到 4 - 5 秒。(统计日期 2021年12月28日,此时有部分已做工作还没有完成)
拿到 playInit 之后就只有两个步骤,new Player(),执行 play() 方法,因此整个的流程图如下:

image.png

然后再看下 Chrome Performance 在 FP 之后的详情截图

有图中看出大任务分别是

  1. sessions 接口请求后的逻辑任务
  2. play/init 接口请求后的逻辑任务
  3. session_events 接口请求后的逻辑任务
  4. 执行 play 方法

网络请求耗时最长的是

  1. 事件流接口(session_events)
  2. 播放初始数据接口(play/init)

结合代码流程图和 Performance 截图,最后总结下当前存在问题:

  • session_events 接口(事件流)当前为异步执行,有可能会出现晚于视频播放的情况,不符合产品需求
  • session_events 接口本身存在数据耦合度太大导致下载时间慢的问题(需要和后端沟通方案)
  • sessions 接口(会话列表)没必要做为前置条件,现在之所以前置是因为其他接口依赖 sessionId,但 获取 sessionId 和 sessions 接口其实没有强关联关系
    • 外部进入情况下,可以直接从 url 参数获取 sessionId
    • 内部切换的情况下,可以直接获取 sessionId
  • session 接口(会话详情)在当前环境下只提供老版本视频资源 url ,和 play/init 功能重复
  • sources 接口和 paly/init 接口只依赖于 sessionId,没必要后置
  • 拿到接口数据后的任务过大,耗时严重,因为播放前所有任务都必须执行完毕,因此异步并不能解决问题

优化方案

  1. 干掉 session 接口(会话详情),资源 url 统一放到 paly/init 接口,减少网络请求(需要后端配合)
  2. 并行执行 sessions 、sources 、session_events 和 paly/init 四个接口,减少相互不必要的等待
  3. session_events 接口变为两个
    • 第一个只获取标题信息减少资源下载时间,另一个获取全量数据(需要支持分页),播放前只通过第一个接口,加载标题信息
    • 第二个接口异步请求全部数据存储到内存,用户展开的时读取,如果用户点击展开的时候全量数据还没有返回,则根据点击项的索引分页请求对应的数据之后再展开,此时全量数据过来后需要有去重策略
  4. 使用 Web Worker 将拿到数据之后的耗时任务丢到子线程中,并行执行
  5. 调度任务 + 时间切片
    • 给不同的任务分配优先级,然后将一段长任务切片
    • 尽量保证重要任务优先执行,其他任务或者无依赖关系任务次要执行(播放主要任务之外的其他任务)
  6. 事件流采用虚拟列表技术,减少渲染消耗(不仅仅解决首次渲染问题,还解决后续滚动性能问题)
  7. 增加缓存,采用 Service Worker 技术,缓存接口请求,正在调研谷歌的 Workbox 类库,当前版本只缓存 sources 、paly/init 和 session_events 三个接口和部分静态资源,后续逐步展开至全站缓存

最终流程图

image.png