优化对象
此次优化是针对,Web 端视频播放页面。白屏时间按照播放视频状态有两种情况,分别是:
外部进入,包括筛选页进入、分享链接进入、用户详情页进入和页面强制刷新,此时白屏时间可以理解为浏览器输入 url 到播放器播放的时间,主要流程分为四部分:
- 完全白屏,FP(First Paint) 之前流程
- 初始渲染视图和 loading 出现,sessions 接口请求极其依赖逻辑执行
- sessions 之后的流程
- loading 消失,开始播放
内部切换,通过手动点击【用户会话】列表项切换视频,此时白屏时间可以理解为 loading 出现到消失的阶段,有三部分:
- loading 出现
- sessions 之后的流程
- loading 消失,开始播放
整理流程图示如下:
预期结果
正常情况下,新版本视频目标达到 秒开,老版本视频因为需要手动解析事件,可能会稍慢一些。
已做工作
- 事件流分页(加载第一页数据后即开始播放)
- 提前渲染,不等待数据准备完毕,第一时间渲染播放器、会话列表和事件流框架,播放前展示 loading ,给用户视觉上更好的体验,「觉得快」
引入 Web Worker
- 新版本视频因为返回数据为字符串,需要手动解析为 JSON,因此下载和解析逻辑移入 Web Worker,充分利用 CPU 资源,避免影响主线程逻辑和渲染,播放前主要是不影响数据流,事件流的解析和渲染。
- 老版本视频下载和解析逻辑移入 Web Worker。
- 全页面去掉自定义滚动条
- 开发者工具 source 面板懒加载
整体分析
借助 Chrome Performance 录制白屏至视频开播之间的片段,进行分析:
因为 FP 用于记录页面第一次绘制像素的时间,之后即会进入动态代码阶段,所以下面以 FP 为界限,分别分析前和后两部分。
FP 之前分析
下面是 FP 之前的详情截图
从图中可以看出耗时的任务主要为两部分:
- my-details.js 文件下载(目前 gzip 压缩之后,体积为737.25 KB)
- 一个大 Task
问题定位
- 文件下载阶段,打包文件过大,通过 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),三个大文件中的较大模块如下:
其中 monaco-editor 是最大的一个问题点。
- js 文件执行阶段,具体分析下大 Task,Performance 放大截图如下
分析 Task 执行顺序,执行流程图如下(只列出主要流程,打包之后执行流程会不同,但不影响分析):
结合 Performance 结果,可以看出其中 monaco-editor 仍然是最大的问题点。
进一步分析问题如下:
chunk-vendor.js
是项目所有的依赖库,从它是打包了node_modules
可以看出,所以很影响性能monaco-editor
编辑器占了很大体积- 对于一些其他的工具库,未采用按需引入的方式
优化方案
优化 FP 指标,关键是缩短资源的下载时间,以及减少阻塞浏览器渲染 DOM 的任务的执行时间,通常来说的手段有骨架图、分块加载、缓存、减少同步js代码、资源压缩和资源按需加载等等。对我们来说大部分工作已经做了,现在关键点为有几个文件很大。
因此,总结下优化方案:
- 拆分过大 js 文件,尤其是
monaco-editor
的抽离,因为用的地方太多了, 导致很难做成按需加载了的了,可以使用splitChunks
分离代码并实现相关模块共享。单独打包monaco-editor
,最终减少请求资源的大小和请求次数 - 替换过大的第三方类库,主要是 lodash.js 和 moment.js
- 工具库采用按需引入的方式,尤其是 element-ui
- 缓存部分静态资源,提高加载速度
ps: Webpack 调优已完成,补充文档 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() 方法,因此整个的流程图如下:
然后再看下 Chrome Performance 在 FP 之后的详情截图
有图中看出大任务分别是
- sessions 接口请求后的逻辑任务
- play/init 接口请求后的逻辑任务
- session_events 接口请求后的逻辑任务
- 执行 play 方法
网络请求耗时最长的是
- 事件流接口(session_events)
- 播放初始数据接口(play/init)
结合代码流程图和 Performance 截图,最后总结下当前存在问题:
- session_events 接口(事件流)当前为异步执行,有可能会出现晚于视频播放的情况,不符合产品需求
- session_events 接口本身存在数据耦合度太大导致下载时间慢的问题(需要和后端沟通方案)
sessions 接口(会话列表)没必要做为前置条件,现在之所以前置是因为其他接口依赖 sessionId,但 获取 sessionId 和 sessions 接口其实没有强关联关系
- 外部进入情况下,可以直接从 url 参数获取 sessionId
- 内部切换的情况下,可以直接获取 sessionId
- session 接口(会话详情)在当前环境下只提供老版本视频资源 url ,和 play/init 功能重复
- sources 接口和 paly/init 接口只依赖于 sessionId,没必要后置
- 拿到接口数据后的任务过大,耗时严重,因为播放前所有任务都必须执行完毕,因此异步并不能解决问题
优化方案
- 干掉 session 接口(会话详情),资源 url 统一放到 paly/init 接口,减少网络请求(需要后端配合)
- 并行执行 sessions 、sources 、session_events 和 paly/init 四个接口,减少相互不必要的等待
session_events 接口变为两个
- 第一个只获取标题信息减少资源下载时间,另一个获取全量数据(需要支持分页),播放前只通过第一个接口,加载标题信息
- 第二个接口异步请求全部数据存储到内存,用户展开的时读取,如果用户点击展开的时候全量数据还没有返回,则根据点击项的索引分页请求对应的数据之后再展开,此时全量数据过来后需要有去重策略
- 使用 Web Worker 将拿到数据之后的耗时任务丢到子线程中,并行执行
调度任务 + 时间切片
- 给不同的任务分配优先级,然后将一段长任务切片
- 尽量保证重要任务优先执行,其他任务或者无依赖关系任务次要执行(播放主要任务之外的其他任务)
- 事件流采用虚拟列表技术,减少渲染消耗(不仅仅解决首次渲染问题,还解决后续滚动性能问题)
- 增加缓存,采用 Service Worker 技术,缓存接口请求,谷歌的 Workbox 类库,当前版本只缓存 sources 、paly/init 和 session_events 三个接口和部分静态资源,后续逐步展开至全站缓存
ps:现阶段缓存已经添加完毕,产出 前端缓存机制提升网站性能 - Service Worker
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。