前言
今日早读文章由腾讯@编译青春投稿分享。
正文从这开始~~
近两年,信息流行业处于一个增长缓慢甚至停滞的状态,包括今日头条、腾讯看点在内的信息流产品都在寻求自己的破局之路。与此同时,抖音、快手等新形态内容却实现了爆发式增长。研究发现,抖音、快手都具有用户覆盖面大、差异化小的普适特点,相比之下虎扑、小红书这类垂直领域的天花板都比较低。什么内容具备普适特点呢?有两类,一类是打发时间、放松解压的搞笑内容,一类是明星八卦、话题谈资的热点内容,这两类内容具有低门槛、 快消费、易传播的特点。为了进一步降低内容消费的门槛,我们把消费场景放在了信息流中,用户无需进入详情页就可以直接消费完文字、图片、动图、视频等内容,这种新的内容形态被称为“短内容”,由短内容构成的信息流被称为“短内容页面”。
QQ 浏览器中短内容页面的入口是在推荐流中的短内容卡片,一般带有分享、评论、点赞互动栏的就是短内容卡片,点击短内容即可以进入短内容页面。
对于 C 端页面,用户体验尤为重要,尤其是首屏体验,更是奠定了用户的第一印象,所以“性能优化/首屏优化”常作为前端人的重要研究课题。那短内容页面的首屏体验是怎么样的呢?
看完上面在手机性能相对较好的 iPhone X 上的演示动图,你一定会感觉到这真是个糟糕的体验,那到底糟糕在哪里呢?具体有三点:白屏时间长、图片加载慢、页面过渡僵硬。本地的首屏优化方案就集中在这三方面,本文也围绕这三点详细阐述。
白屏时间长
所谓白屏,指打开新页面时屏幕中没有任何有意义的内容,只有无休无止令人窒息的空白。造成页面白屏的原因有不少,比如页面崩溃、网络资源加载较慢、页面启动卡顿等等,这里不讨论页面崩溃等程序出现 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,下面的事前端就比较熟悉了,拉取数据,渲染内容。
整个过程可以分为两个部分:页面启动和数据加载,我们分别从这两方面进行优化。
页面启动
页面启动阶段主要有 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 删掉后,打包后的 bundle 体积减少了 2.4%,安卓的启动时长减少了 5.2%,iOS 的启动时长减少了 3.9%。
数据加载
数据加载阶段主要包含 fetch 和 setContentView 两个阶段,逐个分析发现,主要的耗时环节是获取数据环节,那如何优化这部分的耗时呢?我们再次细化获取数据环节,大概分为 DNS - 建立连接 - 后台处理 - 个性化推荐 - 数据返回这几个阶段。网络传输环节与运营商相关,个性化推荐与算法、机器数量和性能相关,即便优化,耗时也很难有实质性的缩减。总而言之,从减少获取数据耗时这一环节本身出发,我们做不了什么。那是否意味着这个环节我们不能优化了呢?方法还是有的,我们可以提前获取数据,然后缓存到本地,等用户打开页面的时候直接从缓存获取第一刷的数据。从缓存读取数据会比从网络读取数据减少不少的耗时,那具体怎么做呢?
短内容页面的入口是推荐流中的短内容卡片,当推荐流中有短内容卡片曝光时,QQ 浏览器将会去网络拉取一刷数据缓存到本地。如果用户真的点击了短内容卡片进入了短内容页面,那么会直接从缓存中获取数据。这个方案被称为“数据预加载”。
道理我都懂,但感觉太简单,里面会不会有坑呢?有的,数据预加载可能存在四个方面的问题。
流量浪费
如果用户没有进入短内容浮层,那这部分流量不是浪费了吗?这里要分两方面看。一方面是公司的流量浪费,公司的流量费用是与公司峰值带宽相关的,数据预加载对公司的峰值带宽影响不大,所以不会导致公司方面的流量浪费;另一方面是用户的流量浪费,据统计有 44% 的短内容用户用流量浏览,对于这部分用户来说,预加载方案是一种以空间换时间的方式,这部分的流量浪费不可避免,并且为了减少流量浪费,我们选择了预加载 3 条而不是更多,因为一般情况下 3 条恰好可以覆盖一屏,包含 1 条主 TL 的帖子和 2 条推荐帖子。
推荐浪费
用户看到的帖子都是由推荐系统推荐的,已经看过的帖子推荐系统就不会再给我们推送了,这叫“曝光去重”。在数据预加载场景下,很可能会出现推荐系统推荐了帖子,但用户实际上没有进入短内容页面也就没有消费这些帖子的情况,这叫做“推荐物料浪费”。针对这种情况,我们与推荐后台约定,预加载出的推荐数据在下一次推荐时不会被曝光去重,只有用户真正消费的时候,前端回写曝光数据,告诉推荐后台哪些帖子被真正消费了,那么这些帖子才会被曝光去重,那么这样也就避免了推荐物料被浪费的情况。
缓存关闭
预加载的数据会被浏览器缓存在内存中,当浏览器运行在前台时,手机分配的内存空间足够;而当浏览器切到后台时,手机分配的内存空间减少,会导致预加载数据的缓存空间被清除,这样不仅之前缓存的预加载数据都被清除,下一次写缓存数据时也会失败。为了解决这个问题,只要每次写缓存时检查一下缓存空间还在不在,在的话就就直接写,不在的话就得重新创建缓存空间。
二次打开
当用户退出短内容页面时,大约会有 3% 的用户会重新进入。第一次打开短内容页面的时候,推荐的 2 条数据会被清除,缓存中只会留下第一条数据。用户第二次进入的时候,只会读取那一条缓存的内容,其余的需要从网络拉取。
Bundle 预加载
分析了首屏渲染的各个关键耗时环节,我们缩减了 bundle 的体积,提升了页面启动的速度;同时对数据进行了预加载,第一刷的数据直接从缓存中读取,提升了数据拉取速度。除此以外,还有其他优化方法吗?减包加速页面启动终究还是有瓶颈,何不一步到位直接让页面运行在后台,等到用户真正点击的时候再把页面提升到前台呢?
这个方案的原理跟手机中的 APP 很相似,如果一个 APP 运行在后台,那被切到前台时将会很快,但如果被第一次打开,耗时将会较长。所谓的 bundle 预加载,就是将短内容页面预先加载在浏览器的后台,等到用户点击打开页面时再显示出来。这一过程的演示:
视频中透明的浮层代表没有任何内容的短内容页面,当浏览器启动时,会在背后悄悄启动一个空白的短内容页面,如果用户点击了入口短内容卡片,那么这个空白的短内容页面将会被提升到浏览器的最顶层,并且被渲染。这样,bundle 加载的耗时将会被大大缩减。除此以外,浏览器还会在后台又启动一个空白的短内容页面,以备下一次用户打开短内容页面使用。
小结
为了解决白屏时间长的问题,我们仔细剖析了页面加载中的每一个环节,其中针对“加载 bundle”和“获取数据”两个关键耗时环节采取了“减包”和“数据预加载”措施,同时也认识到“减包”所带来的收益是递减并且有瓶颈的,所以直接采用“bundle 预加载”的方式,在浏览器启动时准备一个运行在后台的空白短内容页面,用户打开时直接使用该空白页面,大大缩减了页面启动了时间。
图片加载慢
从网络加载图片资源需要一定的耗时,所以时常会出现文字已经展示但图片还是一片灰色的情形。那如何缩减图片的加载时长呢?我们探索出了 4 个方案:图片压缩、图片裁剪、SharpP 和图片预加载。
图片压缩
缩减网络传输时长,最简单有效的方式便是减小网络包的大小,图片也不例外,所以我们很自然地想到了压缩图片。那如何压缩图片呢?得益于腾讯强大的技术基础建设,我们有比较完善的图片服务。
这是一条常见的图片 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 已经足够满足需要了。
图片裁剪
那有没有办法再减小图片的大小呢?有的,图片裁剪。
短内容页面中多图都是以 1 : 1 展示的,也就意味着超出 1 : 1 的部分不仅不会展示,还会增加图片的大小导致图片加载耗时变长。这样的话,我们只需要加载 1 : 1 的部分就可以了。理想很丰满,现实也很性感,强大的图片服务也提供了图片裁剪功能!
不过不是所有的图片都支持裁剪,只有满足业务 ID 为 “qqpubliccover” 并且 File ID 以 “_open” 结尾的图片才能够裁剪。一共支持 16 种裁剪尺寸,239 * 95、358 * 143、564 * 280......其中数字仅表示裁剪比例,不表示图片宽度。
上图显示了按一定比例裁剪后的图片的 3 种样式。
因为多图场景下图片都是 1 : 1 展示的,所以我们只需要拉取对应尺寸的图片,即在 File ID 末尾加上 “280280”。上面的例子中,原图 26KB,裁剪后的图片只有 18KB,图片体积缩减了 30%。
这里需要提一下的是,图片裁剪服务原本的使用场景并不是为了缩减图片体积,而是为了突出图片主体。我们是不是经常遇到因为图片中人物头像在顶部,在信息流中展示时头部被裁切掉,然后只能看到半截身子的情况?裁图服务可以将原图裁剪成不同比例,并且保证每种比例都会突出图片主体,比如人物、动物、物体等等,信息流业务在使用图片时选取一种与展示比例相近的裁剪尺寸,这样展示的图片可以较好地突出主体。下面的例子可以很形象的展示是否使用裁剪过的图片的不同效果,前者使用原尺寸,主体头部被裁切,后者使用 280 * 280 比例的裁剪,主体依旧完整。
SharpP
前面两个都是缩减静图加载时长的方法,那动图呢?
动图我们采用了 SharpP 格式,这是腾讯自研的图片压缩技术,对标业界的 WebP,不过却比 WebP 有更好的压缩效果。
图片预加载
无论是减包还是 SharpP,总会有一定的网络耗时,如果还想减少图片加载耗时应该怎么办呢?没错,预加载图片~
当推荐流中短内容卡片曝光后,浏览器会去请求第一条短内容的图片并缓存起来。短内容页面打开时,直接使用缓存的图片,这样就可以避免长时间只能看到图片灰底的情况。不过为了节约流量,目前只会预加载第一条短内容的图片。下面的例子中,正文图片很快加载完成,与此形成鲜明对比的是尺寸很小的头像,在正文图片加载完成后仍然还是灰底。
小结
为了解决图片加载慢的问题,我们采取了压缩图片和裁剪图片这两个方法缩减静图的体积,而对于动图采用了 SharpP 格式。为了进一步提升首屏体验,我们对第一条短内容的图片进行了预加载。
页面过渡僵硬
从推荐流进入短内容页面过程很僵硬,可以感觉到就是页面间的跳变,用户体验不佳。那该如何优化呢?在调研了浏览器现有的几种页面打开方式后,我们决定采用“侧滑”的过渡动画,即短内容页面从右往左切到浏览器的前台,如图演示:
小结
站在用户角度,以用户的眼光审视短内容页面的首屏体验,我们发现了三大问题:白屏时间长、图片加载慢和页面过渡僵硬。
针对白屏时间长的问题,我们梳理了首屏的关键耗时环节,再分析缩减每个环节耗时的可能性,其中在 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%。
关于本文 作者:@编译青春 原文:https://mp.weixin.qq.com/s/dGO7cvhnr0cWdeu7O2v5Eg
@编译青春曾分享过
【第1944期】HTTP/3原理与实践
【第1171期】npm 2017 JavaScript 框架报告之后端框架
【第884期】你不知道的 DOMContentLoaded
欢迎自荐投稿,前端早读课等你来