B站新版播放页优化总结报告

前言

页面从输入网址到页面加载出第一屏的内容,我们称之为首屏数据,从白屏到页面渲染完成这个过程是有时间消耗的,大多数情况下我们也是压缩这段时间来做首屏的性能,时间越短 页面呈现的越快 用户感受到的体验也会越好。一般网页都会是这样去做的,但播放页的“首屏”略有不同,播放页是以视频为主,页面的核心是视频的画面,所以我们会重新定义播放页的“首屏”为视频的首帧(播放器拉取视频流到可播放时间,大概在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 window === '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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容