“秒开”浏览器实现起来有多难?¶
统计信息:字数 25883 阅读52分钟
作者:billpchen,腾讯看点前端开发工程师
近两年,信息流行业处于一个增长缓慢甚至停滞的状态,腾讯看点一直在寻求自己的破局之路。研究发现,近两年实现爆发增长的业务都具有用户覆盖面大、差异化小的普适特点。
什么内容具备普适特点呢?有两类,一类是打发时间、放松解压的搞笑内容,一类是明星八卦、话题谈资的热点内容,这两类内容具有低门槛、 快消费、易传播的特点。为了进一步降低内容消费的门槛,我们把消费场景放在了信息流中,用户无需进入详情页就可以直接消费完文字、图片、动图、视频等内容,这种新的内容形态被称为“短内容”,由短内容构成的信息流被称为“短内容页面”。
QQ 浏览器中短内容页面的入口是在推荐流中的短内容卡片,一般带有分享、评论、点赞互动栏的就是短内容卡片,点击短内容即可以进入短内容页面。
对于 C 端页面,用户体验尤为重要,尤其是首屏体验,更是奠定了用户的第一印象,所以“性能优化/首屏优化”常作为前端人的重要研究课题。那短内容页面的首屏体验是怎么样的呢?
看完上面在手机性能相对较好的 iPhone X 上的演示动图,你一定会感觉到这真是个糟糕的体验,那到底糟糕在哪里呢?具体有三点:白屏时间长、图片加载慢、页面过渡僵硬。本地的首屏优化方案就集中在这三方面,本文也围绕这三点详细阐述。
1. 白屏时间长¶
所谓白屏,指打开新页面时屏幕中没有任何有意义的内容,只有无休无止令人窒息的空白。造成页面白屏的原因有不少,比如页面崩溃、网络资源加载较慢、页面启动卡顿等等,这里不讨论页面崩溃等程序出现 bug 的场景,有 bug 就去解决嘛。¶
我们都讨厌白屏,有时白屏时间比较短,在我们的容忍范围内,但有时白屏时间很长,那就令人烦躁了。据统计,大多数用户可以忍受 1000ms 以内的白屏时间,超过 1000ms 时随时间的变长越来越无法忍受。所以我们的首屏优化目标是 1000ms 以内,即“秒开”。
定好了目标,下面貌似就应该行动起来寻找解决方案了,但等等,我们一直在说“首屏”,那什么是首屏,我们得先唠唠。首屏的英文名是 “Above the Fold”,Fold?跟折叠有什么关系?“Above the Fold” 这个概念最早用于出版行业,买过报纸的同学都知道,因为方便搬运,报纸的一般都是折叠起来的,即使是“头版”,也分朝上和朝下的一面,如图所示,朝上的那一半被称为 “Above the Fold”,也是被报社认为最重要的位置。延伸到互联网产品,“Above the Fold” 用来指代页面不用操作(比如点击、滚动)就能看到的信息。
明白了“首屏”,那什么是“首屏时长”?虽然“首屏”这个概念是从报纸那里借鉴过来的,但我们看到报纸的头版也就看到了,不存在先看到一部分再看到完整的,所以“首屏时长”是属于互联网产品特有的概念。为了衡量看到“首屏”内容的效率,人们定义了很多标准,比如 Google 就定了 FP、FCP、FMP 等很多指标来衡量首屏的性能,久而久之,这些标准成了大家公认的标准。短内容页面基于 Hippy,一种动态化框架,本身没有什么衡量标准,所以我们就仿照 Chrome,定义了动态化页面的首屏打开性能指标:FCP 和 FMP。
在我们的定义中,短内容页面的 FCP 指的是从外层入口点击的时间点到短内容页面根元素 didMount 的时间点的差值,FMP 指的是从外层入口点击的时间点到短内容第一条卡片根元素 didMount 的时间点的差值。这里的 FCP 和 FMP 可能跟 Chrome 中的定义有所偏差,不过无伤大雅,我们需要的只是有那么几个指标来衡量优化前后的效果。
明确了首屏时长的衡量标准后,那下面我们就可以开始正式的优化环节了。从哪里下手呢?既然要缩减白屏时长,那就要了解首屏加载有哪些环节,辨别出关键的耗时环节才能有的放矢。
如图所示,当用户点击了短内容页面的入口时,客户端开始创建 Activity,然后开始创建 Hippy 引擎,引擎创建完成后加载 Bundle,而后向前端发送 loadInstance 事件开始启动业务,接着便开始渲染 HippyRootView,下面的事前端就比较熟悉了,拉取数据,渲染内容。
整个过程可以分为两个部分:页面启动和数据加载,我们分别从这两方面进行优化。
1.1 页面启动¶
页面启动阶段主要有 initEngine、onInitialized、loadModule 和 loadInstance 4 个阶段,逐个分析发现,在“加载 bundle” 阶段我们可以有所作为。加载 bundle 的时间跟 bundle 体积成正相关,如果我们把 bundle 的体积减小,那么 bundle 自然加载地更快。如何减包呢?我们可以使用 Webpack Bundle Analyzer 对 bundle 进行分析。
从分析结果中,我们可以看到有近 10 个文件的体积超过了 100 KiB,还有很多的文件达到数十 KiB,那开始挨个分析每一个文件。虽然 CircleCommJce.js 体积最大,但项目处于新旧交替过程中,项目原先是通过 WUP 协议拉取数据,而现在随着腾讯看点三端(QQ 看点、QQ 浏览器、看点快报)的统一,CGI 服务也统一成 HTTP,所以这里逐渐会将 WUP 协议换成 HTTP 协议,等项目协议切换完成,这些与 WUP 相关的文件都可以删掉了。与 CircleCommJce.js 类似的还有 MTT4PageInfoJce.js 等等,这些文件我们暂时不作改动。
接着,lodash.js 这个文件引起了我的注意。Lodash 是一个便捷的 JS 工具库,但便捷的代价就是项目体积的增加,这不,lodash.js 就有 466 KiB。那如何减少这个文件的体积呢?很简单,彻底不用。在项目中检索 “lodash”,发现被引用了 19 次,共 45 次调用。乍一看还挺多,仔细梳理,发现一共就用到了 Lodash 的 5 个方法:.get、.chunk、.pick、.pickBy 和 _.mapValues。将这些方法用自己写的 JS 函数替代,比如下面就是 _.get 的简单实现。
将 lodash.js
删掉后,打包后的 bundle 体积减少了 2.4%,安卓的启动时长减少了 5.2%,iOS 的启动时长减少了 3.9%。
1.2 数据加载¶
数据加载阶段主要包含 fetch 和 setContentView 两个阶段,逐个分析发现,主要的耗时环节是获取数据环节,那如何优化这部分的耗时呢?我们再次细化获取数据环节,大概分为 DNS - 建立连接 - 后台处理 - 个性化推荐 - 数据返回这几个阶段。网络传输环节与运营商相关,个性化推荐与算法、机器数量和性能相关,即便优化,耗时也很难有实质性的缩减。总而言之,从减少获取数据耗时这一环节本身出发,我们做不了什么。那是否意味着这个环节我们不能优化了呢?方法还是有的,我们可以提前获取数据,然后缓存到本地,等用户打开页面的时候直接从缓存获取第一刷的数据。从缓存读取数据会比从网络读取数据减少不少的耗时,那具体怎么做呢?
短内容页面的入口是推荐流中的短内容卡片,当推荐流中有短内容卡片曝光时,QQ 浏览器将会去网络拉取一刷数据缓存到本地。如果用户真的点击了短内容卡片进入了短内容页面,那么会直接从缓存中获取数据。这个方案被称为“数据预加载”。
道理我都懂,但感觉太简单,里面会不会有坑呢?有的,数据预加载可能存在四个方面的问题。
- 流量浪费 如果用户没有进入短内容浮层,那这部分流量不是浪费了吗?这里要分两方面看。一方面是公司的流量浪费,公司的流量费用是与公司峰值带宽相关的,数据预加载对公司的峰值带宽影响不大,所以不会导致公司方面的流量浪费;另一方面是用户的流量浪费,据统计有 44% 的短内容用户用流量浏览,对于这部分用户来说,预加载方案是一种以空间换时间的方式,这部分的流量浪费不可避免,并且为了减少流量浪费,我们选择了预加载 3 条而不是更多,因为一般情况下 3 条恰好可以覆盖一屏,包含 1 条主 TL 的帖子和 2 条推荐帖子。
- **推荐浪费 **用户看到的帖子都是由推荐系统推荐的,已经看过的帖子推荐系统就不会再给我们推送了,这叫“曝光去重”。在数据预加载场景下,很可能会出现推荐系统推荐了帖子,但用户实际上没有进入短内容页面也就没有消费这些帖子的情况,这叫做“推荐物料浪费”。针对这种情况,我们与推荐后台约定,预加载出的推荐数据在下一次推荐时不会被曝光去重,只有用户真正消费的时候,前端回写曝光数据,告诉推荐后台哪些帖子被真正消费了,那么这些帖子才会被曝光去重,那么这样也就避免了推荐物料被浪费的情况。
- **缓存关闭 **预加载的数据会被浏览器缓存在内存中,当浏览器运行在前台时,手机分配的内存空间足够;而当浏览器切到后台时,手机分配的内存空间减少,会导致预加载数据的缓存空间被清除,这样不仅之前缓存的预加载数据都被清除,下一次写缓存数据时也会失败。为了解决这个问题,只要每次写缓存时检查一下缓存空间还在不在,在的话就就直接写,不在的话就得重新创建缓存空间。
- 二次打开 当用户退出短内容页面时,大约会有 3% 的用户会重新进入。第一次打开短内容页面的时候,推荐的 2 条数据会被清除,缓存中只会留下第一条数据。用户第二次进入的时候,只会读取那一条缓存的内容,其余的需要从网络拉取。
1.3 Bundle 预加载
分析了首屏渲染的各个关键耗时环节,我们缩减了 bundle 的体积,提升了页面启动的速度;同时对数据进行了预加载,第一刷的数据直接从缓存中读取,提升了数据拉取速度。除此以外,还有其他优化方法吗?减包加速页面启动终究还是有瓶颈,何不一步到位直接让页面运行在后台,等到用户真正点击的时候再把页面提升到前台呢?
这个方案的原理跟手机中的 APP 很相似,如果一个 APP 运行在后台,那被切到前台时将会很快,但如果被第一次打开,耗时将会较长。所谓的 bundle 预加载,就是将短内容页面预先加载在浏览器的后台,等到用户点击打开页面时再显示出来。这一过程的演示:
视频中透明的浮层代表没有任何内容的短内容页面,当浏览器启动时,会在背后悄悄启动一个空白的短内容页面,如果用户点击了入口短内容卡片,那么这个空白的短内容页面将会被提升到浏览器的最顶层,并且被渲染。这样,bundle 加载的耗时将会被大大缩减。除此以外,浏览器还会在后台又启动一个空白的短内容页面,以备下一次用户打开短内容页面使用。
1.4 小结¶
为了解决白屏时间长的问题,我们仔细剖析了页面加载中的每一个环节,其中针对“加载 bundle”和“获取数据”两个关键耗时环节采取了“减包”和“数据预加载”措施,同时也认识到“减包”所带来的收益是递减并且有瓶颈的,所以直接采用“bundle 预加载”的方式,在浏览器启动时准备一个运行在后台的空白短内容页面,用户打开时直接使用该空白页面,大大缩减了页面启动了时间。
2. 图片加载慢¶
从网络加载图片资源需要一定的耗时,所以时常会出现文字已经展示但图片还是一片灰色的情形。那如何缩减图片的加载时长呢?我们探索出了 4 个方案:图片压缩、图片裁剪、SharpP 和图片预加载。
2.1 图片压缩¶
缩减网络传输时长,最简单有效的方式便是减小网络包的大小,图片也不例外,所以我们很自然地想到了压缩图片。那如何压缩图片呢?得益于腾讯强大的技术基础建设,我们有比较完善的图片服务。
这是一条常见的图片 CDN 地址,主要包含 CDN 域名、图片平台业务ID、File ID 和压缩宽度几个部分,我们关心的图片压缩就跟这最后一个部分相关。
在上面的链接例子中,/0 指的是原图,除了 /0 外,还有 /900、/600、/320、/200 和 /180 共计 6 种配置,/900 的意思是图片最大的宽度是 900 像素,如果原图宽度大于 900 像素就会被等比缩放到 900 像素,如果小于 900 像素则保持不变。
那问题来了,该选哪一种压缩比例呢?
以 iPhone 11 为例,横向上像素值为 828。如果图片宽度大于 828,那么在 iPhone 11 上展示时就很清晰;如果图片宽度小于 828,那么在 iPhone 11 上展示就会被拉伸,原图宽度越小模糊感就越强烈,所以要想图片在 iPhone 11 上展示清晰,需要宽度大于 828 像素,所以只有压缩比例为 /0 或 /900 的图片符合要求,挑选最小的尺寸即可,即 /900。同样的,如果手机屏幕宽度只有 400 像素,那么图片的宽度大于 400 像素即可,/0、/900 和 /600 都符合要求,我们选择尺寸最小的 /600。
明白了这个道理,我们看实际应用中的情况。上面的例子中是双图的场景,即两张图片并列排版,这样每张图片的展示区域最大是 828 / 2 = 414,也就是图片的宽度大于 414 像素就足够清晰了,满足条件的有 /0、/900 和 /600 三种压缩尺寸,我们选择最小的 /600。
再看三图的场景,每张图片的展示区域最大只有 828 / 3 = 276 像素,所以 /320 已经足够满足需要了。
综上所述,图片的压缩尺寸由图片的展示区域决定,而展示区域由手机分辨率和图片的排版决定。
2.2 图片裁剪¶
那有没有办法再减小图片的大小呢?有的,图片裁剪。
短内容页面中多图都是以 1 : 1 展示的,也就意味着超出 1 : 1 的部分不仅不会展示,还会增加图片的大小导致图片加载耗时变长。这样的话,我们只需要加载 1 : 1 的部分就可以了。理想很丰满,现实也很性感,强大的图片服务也提供了图片裁剪功能!
不过不是所有的图片都支持裁剪,只有满足业务 ID 为 “qq_public_cover” 并且 File ID 以 “_open” 结尾的图片才能够裁剪。一共支持 16 种裁剪尺寸,239 95、358 143、564 * 280……其中数字仅表示裁剪比例,不表示图片宽度。
上图显示了按一定比例裁剪后的图片的 3 种样式。
因为多图场景下图片都是 1 : 1 展示的,所以我们只需要拉取对应尺寸的图片,即在 File ID 末尾加上 “_280_280”。上面的例子中,原图 26KB,裁剪后的图片只有 18KB,图片体积缩减了 30%。
这里需要提一下的是,图片裁剪服务原本的使用场景并不是为了缩减图片体积,而是为了突出图片主体。我们是不是经常遇到因为图片中人物头像在顶部,在信息流中展示时头部被裁切掉,然后只能看到半截身子的情况?裁图服务可以将原图裁剪成不同比例,并且保证每种比例都会突出图片主体,比如人物、动物、物体等等,信息流业务在使用图片时选取一种与展示比例相近的裁剪尺寸,这样展示的图片可以较好地突出主体。下面的例子可以很形象的展示是否使用裁剪过的图片的不同效果,前者使用原尺寸,主体头部被裁切,后者使用 280 * 280 比例的裁剪,主体依旧完整。
2.3 SharpP¶
前面两个都是缩减静图加载时长的方法,那动图呢?
动图我们采用了 SharpP 格式,这是腾讯自研的图片压缩技术,对标业界的 WebP,不过却比 WebP 有更好的压缩效果。
2.4 图片预加载¶
无论是减包还是 SharpP,总会有一定的网络耗时,如果还想减少图片加载耗时应该怎么办呢?没错,预加载图片~
当推荐流中短内容卡片曝光后,浏览器会去请求第一条短内容的图片并缓存起来。短内容页面打开时,直接使用缓存的图片,这样就可以避免长时间只能看到图片灰底的情况。不过为了节约流量,目前只会预加载第一条短内容的图片。下面的例子中,正文图片很快加载完成,与此形成鲜明对比的是尺寸很小的头像,在正文图片加载完成后仍然还是灰底。
2.5 小结¶
为了解决图片加载慢的问题,我们采取了压缩图片和裁剪图片这两个方法缩减静图的体积,而对于动图采用了 SharpP 格式。为了进一步提升首屏体验,我们对第一条短内容的图片进行了预加载。
3. 页面过渡僵硬¶
从推荐流进入短内容页面过程很僵硬,可以感觉到就是页面间的跳变,用户体验不佳。那该如何优化呢?在调研了浏览器现有的几种页面打开方式后,我们决定采用“侧滑”的过渡动画,即短内容页面从右往左切到浏览器的前台,如图演示:
4. 小结¶
站在用户角度,以用户的眼光审视短内容页面的首屏体验,我们发现了三大问题:白屏时间长、图片加载慢和页面过渡僵硬。
针对白屏时间长的问题,我们梳理了首屏的关键耗时环节,再分析缩减每个环节耗时的可能性,其中在 loadModule 环节我们采取了减包的方法,在 fetch 环节我们采取了数据预加载的方法。更进一步的,我们通过 bundle 预加载的方法,浏览器在启动时会在后台加载一个空白的短内容页面,用户点击短内容卡片时,再将空白的短内容页面提升到前台并且渲染数据。
针对图片加载慢的问题,我们采用图片压缩和图片裁剪的方式缩减信息流中的图片体积,同时使用 SharpP 格式替代传统的 Gif 来缩减动图的体积。更进一步的,为了首屏的体验,我们预加载了第一条短内容的图片。
针对页面过渡僵硬的问题,在调研了浏览器现有的几种页面打开方式后,我们采用了新页面从右边“侧滑”的过渡动画。
经过这一系列的优化,首屏性能指标和产品指标都有显著的提升。
先看平均耗时。在优化前,首屏 FMP 平均为 2.9s,接入数据预加载后平均 FMP 为 0.75s,再接入 bundle 预加载后平均 FMP 为 0.6s。
再看耗时分布。在优化前,FMP 在 0.5s 内的访问占 0%,1s 内占 2%,2s 内占 39%;接入数据预加载后,FMP 在 0.5s 内占 44%,1s 内占 82%,2s 内占 97%;再接入 bundle 预加载后,FMP 在 0.5s 内占 59%,1s 内占 87%,2s 内占 98%。