前言
页面从输入网址到页面加载出第一屏的内容, 我们称之为首屏数据, 从白屏到页面渲染完成这个过程是有时间消耗的, 大多数情况下我们也是压缩这段时间来做首屏的性能, 时间越短 页面呈现的越快 用户感受到的体验也会越好. 一般网页都会是这样去做的, 但播放页的 "首屏" 略有不同, 播放页是以视频为主, 页面的核心是视频的画面, 所以我们会重新定义播放页的 "首屏" 为视频的首帧 (播放器拉取视频流到可播放时间, 大概在 200KB 左右) 可想而知 这个定义下的 "首屏" 参考要比一般网页的首屏的要严格, 不仅仅是视觉上的可见, 还有交互上的可播(播放按钮可以点击).
发个图先
截止十月初
image.PNG
上图为 2018 年 1 月份~ 10 月份的播放页首帧数据优化图(最终指标参考蓝色线条). 从图上可以看到有意思的一点, 在优化过程中数据忽高忽低, 但整体是下降趋势, 最终还是下降到 1.5s 一下, 完成 Q3 优化任务. 这个也反映了 我们在做页面优化的时候不一定所有的想法都会是正确的, 也是通过不断的尝试, 试验, 最终留下可行方案.
网上有很多网页性能优化方案一搜一大堆, 一些基础常用的优化我就不在这里赘述了, 下面说的是我们觉得收益不错且有针对性的一些优化.
首屏直出
虽然前后端分离给开发体验和职责分离上来了很多的好处, 但是在页面渲染和用户体验上却有着天然的劣势, 模板和数据分开处理了, 请求页面得到的只是一个 html 容器, 还要异步去请求接口, 数据成功返回了才能做处理, 硬是吧一个同步的逻辑变成了异步, 最后还要走一遍业务逻辑 + 生成 DOM 然后插入到容器里渲染, 整个过程会有长时间的白屏等待时间, 体验很差.
为了第一时间让页面上有东西呈现出来, 优化方案里首屏直出一定是第一要做的, 直接输出 HTML 和数据(INITIAL_STATE) 让页面同步渲染, 极致的做法可以只输出首屏需要的 DOM 和 CSS, 这个时候整个页面的骨架等都已经出来, 用户体验上省去了等待白屏时间或旋转不停的 Loading 图.
在 Node 介入之前, 播放器页面的组件也是基于 vue 来做的, 所以在选型上用的是 Node+vuejs+Memcached(MC)这样一套组合来实现的 SSR, 完成了首屏直出需求的同时也实现了前后端同构的目的. 也正是有了这些能力, 后面我们很多的优化都基于 node 服务端来做的.(这里主要讲页面优化, 后面如果有机会在新起一篇单独说下 SSR 和缓存之类的)
请求合并 特殊的页面特殊处理
先看下播放页的页面数据组成部分(除播放器相关)
接口: 稿件信息(view) , 推荐列表(related),up 主信息(card), 视频标签(tag), 热门评论(comment)
根据业务需要播放页承担了很大的 SEO 的职责, 所以上面的接口信息基本是要在模板里面带出来的(给爬虫) 那么这时候就有个很明显的问题, 多个接口的情况下而且接口之间可能还有依赖关系, 很难保证这些接口返回的效率, 如果返回的太慢 QPS 一定会很低, 所以聚合接口也是我们的首选, 但又担心一个问题是后端会帮我们去合并接口吗? 虽然我们的后端同学都很好沟通 后面才觉得 对于公司的重点核心项目来说, 做的再多也不为过, 因为一切的努力, 受益的是用户. 很顺利五个接口合五为一, 由于后端走的是内网请求且做了一些缓存优化, 聚合后的接口也是非常的快, 前端 node 拿到接口大概控制在 20ms 以内.
跳出框架
vuejs 是个好东西, 提升开发效率同时, 又是面向组件化模块化开发, 数据驱动, 生命周期等, 给开发带来很多的便利. 这么完美的一个框架, 我实在想不出或不愿意去想它有哪些 "劣势". 直到我们在给页面做优化的时候, 你就会发现它原先的一些优势会在这个问题 (极致优化) 下可能是一个劣势.
简单的列一下播放页中碰到的 "劣势":
一, 在做服务端渲染 (ssr) 的时候, 我们在渲染组件的时候发现是在太弱了, 与常见的服务端模板 (ejs,pug...) 简直不能比 (如图: 压测)
image.PNG
我们先对 node 单点做了一次压测, 测试的页面是真实的播放页. 发现渲染的耗时是随着并发数量的增长, 而增加. 如果想使渲染时间保持在 1 秒以内, 那么并发数的极限是 40 个, 难道只能无限堆机器? 这肯定不是个办法.
40 个开什么玩笑, 要知道播放页每天的访问请求是亿级的, 有时候来个搜索爬虫, 那还不直接瘫痪.(后面当然解决了, 在这个标题下就不详细说了.)
二, 用了 vue 那肯定是按照产品模块一个个的开发组件, 那播放器那块肯定是一个单独的如 VideoPlayer.vue, 页面接入播放器是以 SDK 的形式接入 代码应该如下:
- <template lang="html">
- <div id="bofqi" ref="bofqi"></div>
- </template>
- <script>
- export default {
- props: {
- aid: {
- type: Number,
- default: null
- },
- cid: {
- type: Number,
- default: null
- }
- },
- mounted(){
- // 初始化播放器
- new EmbedPlayer("player", `cid=${this.cid}&aid=${this.aid}&autoplay=true`)
- }
- }
- </script>
上面是一段伪代码 初始化播放器需要传入 aid 和 cid 并且页面上必须要有一个 id 为 #bofqi 的容器 这样播放器就会正确的被实例化
emmm 看着很正常没毛病的一段, but! 初始化播放器是在 vue 的 mounted 生命钩子里面(也只有这里才能拿到 #bofqi 的容器) 那意味着要等组件完全渲染完成后才能初始化播放器. 可怕~ 看图:
这样的话, 播放器要在很后面才能去初始化, 后面还有请求接口, 拉视频流等画面出来那就很后面了, 花都谢了~
image.PNG
所以这里需要改进优化下, 最直观就是尽快初始化播放器最好是和模板页面同步执行. 这里我们尝试了两种优化方式:
一, 把 #bofqi 容器提出来放到 HTML 模板里, 然后在容器的 dom 注入一段初始化播放器的代码, 事实上我们有几个版本上的优化就是这么做的. 如下(伪代码):
- <body>
- ...
- <div id="app">
- </div>
- <script src="//s1.hdslb.com/bfs/static/player/main/video.js">
- </script>
- <div id="bofqi">
- </div>
- <script>
- new EmbedPlayer("player", `cid = $ {
- cid
- } & aid = $ {
- aid
- } & autoplay = true`)
- </script>
- ...
- </body>
这里有个小问题, 就是播放器这个容器的定位, 因为脱离了了文档流, 是悬浮在整个页面之上的, 在前面的几个版本当中播放页只有宽窄两个屏幕的定位 所以兼容上还好, 固定写死两个尺寸下的 CSS 就好了. 大大提前了初始化的时机, 上线后效果很好, 基本上 SDK 下载完成就开始初始化了, 不用等页面渲染.
image.PNG
但随着产品需求的迭代, 要求播放器尺寸自适应, 这就麻烦了, 很明显上面这个方案行不通, 再也不能写死尺寸来适应定位了, 那就只能把 #bofqi 容器再放回 vue 组件里, 这个问题当时确实纠结过一阵, 不过最后还是和架构的同学一起讨论想出了个黑科技, 用过 vue 的同学都知道在 template 里面是无法插入 script 标签的. 但是为了满足自适应只能把容器放回文档流中, 同时还需要插入一段初始化播放器的 JS 代码. 解决方法如下:
App.vue
- <template>
- ...
- <div class="player-wrap">
- <!-- bofqi 容器放回到组件中 -->
- <div id="bofqi"></div>
- <div v-HTML="innerScript"></div>
- </div>
- ...
- </template>
- <script>
- export default {
- data() {
- return {
- innerScript: ''
- }
- },
- created() {
- // created 钩子会在服务端执行 所以这需要判断
- if(typeof Windows === 'undefined') {
- this.innerScript = `<script type="text/javascript">
- new EmbedPlayer("player", `cid=${cid}&aid=${aid}&autoplay=true`)
- <\/script>`
- }
- }
- }
- </script>
利用 created 钩子和 v-HTML 的特性在服务端给 template 注入一段脚本. 目的达到, 而且在客户端 vue 对比的时候也会通过验证. 完美解决~
总结: 框架是个好东西, 可以解决我们在开发中面临的大部分问题, 但不是所有问题.
注意: 注入代码最后 script 的格式<\/script>
加载和执行避让 控制每一个请求
任何一个页面都是由很多个模块组合而成的, 这里面可以把所有模块做个分类, 挑出核心的, 重要的, 一般的, 然后排个优先级, 这个标准的衡量可以站在用户的角度出发, 视频播放页的核心当然是播放器模块, 除此之外其他的都可以算是修饰的, 视频信息, up 信息, 评论, tag, 推荐列表等之类的都可以排在后面一个级别.
我们都知道 JS 是单线程的, 是一件一件的来处理事情, 放在最前面的优先处理. 所以在店里只有一个服务员的时候, 排队往往是最高效的, 那么同理在页面加载资源和 JS 执行的时候也可以这样去给它们排个队.
资源加载上虽然浏览器里面可以同时发出多个请求 同步操作, 但是要考虑到 80 分位以上偏后的用户可能他们的电脑性能和带宽的情况下, 请求的并发可能会带来更糟糕的体验, 同时处理多个任务, 电脑会更加卡顿, 在这里同步并没有优势, 所以让资源的加载也来排个队是很有必要的. 另外如果 JS 模块没有特意去设计的时候, 大多数 JS 文件在下载下来的瞬间其实就自执行了比如 manifest/vendor 等, 在不影响开发的体验上很难改变 JS 的执行时机, 基于以上我们想到的是用钩子函数去回调, 然后把要控制的部分做成串行执行, 这样就能控制这个时间段的所有资源的下载和执行了. 流程图如下:
image.PNG
上图中可以看到, 页面的第一个请求 HTML 模板下载 在解析过程中遇到播放器的 SDK JS 资源然后同步下载 下载完成后 立即初始化播放器 播放器内开始发送请求视频流, 第一帧回来后触发钩子(PlayerMediaLoaded 页面和播放器约定好的) 下载页面其他资源, 因为是服务端渲染所以这里不用担心页面上什么都没有, 骨架和一些必要的信息都已经出来了. 为了确保页面的完整性, 这里做了个兜底, 及时钩子没有生效 4 秒后也会触发下一步操作. 这样一来就把所有不需要第一时间的请求和执行延后处理了, 优先保证了播放器视频首帧先出来.
细节处理, 为了让播放器相关的资源优先和真正控制好这个时间段内的每个请求, 应尽量避免模板里面带出来资源加载比如 图片 小图标之类, 除非这些是 SEO 必需的或者是首屏关键用到的 CSS 背景图之类, 能没有就没有, 否则就不好真正控制到每一个请求的发出.
资源脚本延后加载执行
这个优化其实也是控制好每个资源加载和执行的时机, 单独拎出来说是想举两个例子.
第一个, 在做优化的过程中, 也有其他的任务混进来, 由于我司的 PV 上报收集是基于前端的 JS 脚本来做的上报, 有一段时间 PV 的数据波动较大, 怀疑是上报脚本出了什么问题, 所以就在线上埋了个 204 请求来校验和上报脚本 PV 的差值来对比, 当时想一个 204 请求是很快的而且不需要返回, 结果上了第二天看数据, 整个 80 分位播放页首屏数据增加了近 100ms, 但 50 分位基本上没什么变化, 204 下了数据就回复了. 这个结果告诉我们在 80 分位 + 的段位上, 任何风吹草动对首帧数据的影响来说都有可能被放大
另外一个, 由于播放页里有些其他模块 (广告, 评论等) 需要用到 jQuery, 所以一直没有去掉, 默认情况下也是放在 head 标签里面比较靠前的位子, 感觉这个可以把它后置到首屏完成以后再加载, 所以我们花了一些时间改造, 关键模块不在依赖 JQ, 做了后置处理, 第二天的数据大概下降了 70ms 左右.
文件体积优化
减少文件体积是一个通用的优化, 体积的大小直接影响到网络下载的时长, 减少控制文件大小的方式也有很多, 比如服务端常用的文件压缩(gzip,br...), 前端去不必要的依赖, 框架等, 很多 JS 框架也是尽量精简来获得市场优势, 文件体积的影响对于 PC 上的影响还算可以, 如果是 H5 那是必须严格控制的, 网络环境的差异也直接反应到页面加载速度的体验, 同样在极限优化的情况下, 不管是 H5 还是 PC 那肯定是尽量精简.
最初的播放器 JS 的 sdk 核心库大概在 1M 左右, 里面不仅是视频初始化还有弹幕, 高级弹幕, 解码, 各种设置等等, 其实完全没必要, 后来播放器组的同学也给播放器做优先级划分, 把功能拆开, 做了很多工作大幅度精简核心库最后优化至 200K 左右, 体积小了好多倍, 80 分位速度提升明显.
还有 HTML 模板, 这是第一个请求, 后面发生所有的资源请求都基于它, 所以它的体积也尤为重要, 所以在这个里面我们除了该有的骨架, 数据和 SEO 需要的部分尽量精简, 包括把顶导做成后置 SDK 引入的方式, 也是为了减少体积, SSR 的时候页面上会输出大量的数据(INITIAL_STATE), 其实有些字段用不上, 所以在服务端渲染的时候要清洗一下, 能删的地方都删了, 最终从几十 K 优化到 十几 K.
前置 playurl(Node)
正如前面所说, 播放页是基于 node 服务做的 ssr,node 这一层也是我们前端负责的, 所以可以很好的利用这个点去做一些事情. 先看下播放器从初始化到出现首帧有哪些关键步骤. 如下图:
image.PNG
从图上可以看到, 把一个前端请求的接口后置到后端去处理, 这样播放器去请求视频流的时候直接从模板里拿到了地址, 不用再去发异步. 一个前端异步请求的接口一般情况在几十到上百毫秒, 放到后端去处理走的还是内网接口(后端对内网接口返回时长控制在 20ms 以内) 又更快了.
这个优化有个注意的点, 因为播放页是 ssr 同构的方案, 在页面降级的情况下服务端是不会输出数据的, 页面会走客户端渲染, 这时候 playurl 就拿不到了, 所以还是会和播放器本身配合的去做, 播放器之前的逻辑还是不能去掉, 需要做个判断, 如果页面上能获取到视频流的地址就直接用, 否则自己发个请求去获取.
推荐列表预取 playurl
当你能提前知道用户在页面上的浏览行为的时候, 其实你也可也以做点事情, 比如播放页的右侧视频推荐列表就是个很好的场景. 通过数据发现播放页内部跳转的点击流量很大, 大部分来自于推荐列表位 如图:
image.PNG
上图为播放页推荐列表的点击热力图, 从图上可以清晰的看到靠在前面的视频卡片点击量很大(和它在第一屏有直接关系) . 利用这个点 我们可以给这些点击量大的卡片去预取视频地址, 这里我们是取了前 4 位的地址.
细节点: 在预期地址发请求的时候需要判断下当前视频和页面的状态, 最佳时间是页面空闲状态下, 避免和其他任务并行, 影响其他正常模块的性能.
SPA 重载优化
与之前的版本 (17 年之前) 相比, 新版播放页已经是一个单页应用了, 点击视频推荐列表, 所有模块局部更新, 不用跳新窗口再走一套了, 最重要的是播放器不用重新初始化和资源不用重新加载了. 这样一来整个页面就能省下很多前置的耗时.
虽然说页面已经 SPA 了, 但是在不注意的情况下也会有一些性能上的问题, 如图:
image.PNG
上图分为两个部分, 优化前和优化后, 在优化前 每次用户点击推荐列表后路由先切换, 因为页面上的模块的更新基本上都是基于 watch 路由上 aid 来做改变的, 再由 aid 去请求播放页信息接口, 获取到对应的 cid 然后传给播放器重载视频. 这里会有个问题每次都会消耗一个接口请求的时间来获取播放器重载必要的 aid 和 cid. 其实我们在推荐列表里面是能直接获取到视频的 aid 和 cid 的.
我们故技重施优化了一波. 直接把 aid,cid 传到播放器 先实现重载, 在首帧回来之前我们页面上所有需要更新模块不触发更新, 等待播放器钩子通知再去做各自组件的更新. 所以组件里之前的 watch aid 的方法都变成了事件的方式来通知.
这里主要还是用到了上面说的资源加载和执行的避让以及一些逻辑处理上的优化, 最终给重载事件带来了 100 多毫秒的收益.
静态资源 Prefetch
预加载静态资源已经是优化中常见的一种优化手段了, 通过流量较大的入口页面给需要优化的页面带资源缓存, 这样在用户访问播放页的时候就不需要重新请求资源而是从本地获取(from disk cache), 由于 prefetch 的优先级比较低,(network -> priority: Lowest), 所以不用太担心当前页面的加载带来的性能问题. 也可以通过多个页面一起来做, 首页, 搜索, 空间这些页面都是 PC 上的大流量页面而且有大量的视频开片往播放页跳转, 所以非常适合. 代码:
<link rel="prefetch" as="script" href="//s1.hdslb.com/bfs/static/player/main/video.js">
之前查过一段播放页的数据, 结果显示带有这些页面 reffer 的播放页要比其他的请求快 200ms 左右.
阶段总结
在技术没有本质变化的情况下, 优化并不是什么特别高深的技术, 大多数情况都是你愿不愿意去想且去做的事情, 有很多非常细节的地方可以去做优化, 有时候我们犹豫不决, 经常会感觉收益不确定, 或者是这个优化可能会对现有代码的整洁性造成很大破坏, 后面不好维护等等, 不太想去做. 其实这个时候可以通过实验的方式来验证, 最终你会得到一个收益值和付出成本衡量来决定是否需要采用这个方案. 从本质上讲作为一个前端开发 凡是对页面性能有提升, 对用户体验优化的事, 我们应该尽全力去做, 特别是这种流量巨大的页面, 可能我们优化了一点点, 但收益是千千万. 在一些重要的页面上, 代码的维护性, 扩展性都不是我们首要考虑的, 我们在乎的重中之重应该是页面的性能和用户的体验.
先分享这么多 完结撒花~
哔哩哔哩 (゜ - ゜)つロ 干杯~ 2233
来源: http://www.jianshu.com/p/7b267b22bb54