标签:启用 jsb 根据 数据 切割 数组 wem 代码仓库 成本
0.3s完成渲染!UC信息流正文“闪开”优化实践图片
作者|UC信息流团队 庞锦贵
编辑|Yonie
在上周的 GMTC 全球大前端技术大会上,阿里巴巴前端技术专家庞锦贵发表了《0.3 秒完成渲染!信息流内容页“闪开”优化总结和思考》的演讲,本演讲通过从浏览器内核到客户端外壳、从服务端到前端等多端协作下所实现的日均 N 亿级 PV 页面“闪开”体验所采用的优化策略,及其背后技术策略的思考和选择。本文整理内容如下。
当我们在实施前端性能优化的时候,为了更好地解决白屏问题,现在大家可能会逐渐放弃了前端异步渲染(CSR),而选择尝试 *** 或 PWA 等为优化的方向,甚至考虑切换至 Weex 技术栈下解决问题。然而,这些优化的手段是否能完全解决“白屏”的问题呢?
本文将通过信息流场景下正文“闪开”优化的技术选型及策略演进,给大家提供 一种通过 Web 技术实现 Native 体验的但又区别于 ***、CSR 以及 PWA 的渲染思路——NSR,并以前端开发者的视角来阐述其背后的设计及思考。
闪开:即用户一点即开,表示极致的 Web 页面加载体验。
NSR:Native side rendering,即由客户端(Native 侧)实现页面结构的拼接,进而实现页面渲染的处理技术。
***:Server side rendering,即服务端渲染,由服务器完成页面的 html 结构拼接的页面处理技术。
CSR:Client side rendering,即客户端渲染,指的是由页面在用户的浏览器环境中通过执行 JS 完成页面结构拼装的渲染处理技术。
PWA:Progressive Web Apps,渐进式 Web 应用。
图中左边为优化后的效果
技术指标:T2 秒开率从 44% 提升到 91.4%,T2 绝对值从 1515ms 降低到 320ms;正文加载错误率降低 97%; 闪开率 (0.3s 打开比例) 占比 79%。
备注:T2 指标定义为网页开始加载到首屏渲染结束,这是由 UC 内核自定义的技术统计指标。
当谈论性能优化的时候,必定要考虑对应的场景,如果不交代一下优化目标的情况和背景,可能不太好理解我们为啥会设计 NSR 渲染机制。
首先,我们的目标场景如下图所示:
图片
信息流的内容主要分类图文、视频、图集等几大类。其中图文是信息流最为成熟的内容形式,有约三分之二的下发内容属于此类型,而本文主要讨论的是对于图文类型内容页的优化,我们的目标是图文页的打开体验能达到“闪开”。
当然,我们的优化并不是从 0 开始的,之前做过一轮大规模的体验优化,基本上前端常用的优化手段都用了,而且包括了部分客户端的优化:
正文 Ajax 的预加载:在列表点击进入正文的同时,将正文的 Ajax 数据请求发出去,打开正文后通过请求复用获得 Native 拿到的数据;
首图的预加载:在列表下拉刷新后,由客户端将可视范围内的正文的首图提请下载本地缓存,正文渲染时复用此缓存。
图片
以上是我们早在 2016 年曾做过的优化,当时将 T1(绘制首帧的时间) 性能从全网 2 秒优化到 1 秒左右,刚优化完的 T1 秒开率一度到达了 90%+。
但历史的优化也遗留了一些未解决的问题,而随着业务的迭代,秒开比例逐步降低到了“闪开”优化前的 70% 左右。
不是我们没有考虑过对主文档进行缓存控制,原因有以下 2 个:
为什么不是 Weex
我们曾思考将正文 Weex 化的可行性,并且在商品导购类页面上采用了 Weex 去落地,也拿到了比较不错的结果,然而正是由于这一尝试,我们否定了将信息流内容页迁移到 Weex 技术体系的可能。
之所以不选择 Weex,有以下 5 个原因:
跨端:除了 UC 端内,正文还需要考虑第三方 APP 打开的场景,如外渠、分享等,正文页面的运行环境是非常多变的;
跨平台:Weex 兼容 IOS、安卓和 H5 的成本不可忽视,其降级 H5 的体验需要定向优化,比如左右滑屏、视差滚动、复杂动画;
排版:Weex 处理富文本内容有短板,排版的表现力满足不了正文场景的诉求,而解决此问题的成本是比较高的;
性能:虽然 Weex 性能好于 H5(好的最主要原因是 Weex 资源是离线的),但其本质是依赖于 JSBridge,性能与 Native 仍有差距,当我们要求对齐 Native 的体验时,Weex 也要彻底解决页面打开的闪白问题;
成本:正文要覆盖端外没有 Weex 容器的场景,必然要很好地兼容 H5 回退的情况,并且体验必须不能低于现有的 H5 体验。也就是如果选择 Weex,总体成本必然大于只维护 H5 版本的情况,而且业务是不停在迭代的,如果用 Weex 重构整个 H5 体系,其风险和成本几乎是不可控的。
因此,容器选择是显而易见的,只有 H5 一条路。在 H5 容器下,我们只需要重点解决性能的问题即可。
在上面的背景中,我们提到了信息流正文是采用 CSR 模式渲染的,在这个模式下遗留了几个没有解决的问题。既然 CSR 存在问题,而前端擅长的手段还有 PWA 或 *** 都可选项。当然,结果最开始已经说了,我们既没选择 PWA,也没有选择 ***,原因分别是:
图片
PWA 对信息流业务有用的 ServiceWorker;
ServiceWorker 的启动和保活的成本很大 ;
ServiceWorker 的缓存——CacheStorage,本质上也是 HttpCache,在内核角度并不是最快的,而且如果我们使用 SW 对正文 Cache 后,如何更新和回收成为一个必须解决的问题。
图片
不选择 *** 的原因
既然 PWA 并不适合信息流场景,*** 就应该纯前端性能优化的最优手段了吧?那么,它能对齐 Native 的性能吗?
图片
理论上,从点击到 Window 动画结束即可完成渲染,而过程中要消除白屏的感觉,还需要在滑动出来之后尽快有内容。当然不一定是整页,但好的体验应该是尽快让用户看到内容,而此过程耗时要小于等于300 毫秒!
如果这一标准标准去衡量 *** 后的正文加载,在没有其他优化手段的辅助的前提下,*** 也几乎不太可能达成 300ms 完成加载和渲染,因为即便不考虑页面渲染绘制的耗时,可能主文档或 Ajax 请求的网络耗时就不止 300ms 了。
为了达到 300ms,我们可以需要将页面从点击到渲染的整个流程进行拆分,将其分为三个阶段:
Native 的耗时 ≈ WebWindow 创建 + WebView 初始化
网络的耗时 ≈ DNS + TCP(SSL) + 服务端渲染耗时 (如果采用 *** 的话) + Document 下载
渲染的耗时 ≈ Ajax(CSR) + JS scripting(CSR 耗时) + 浏览器 Painting(内核)
问题 1: WebWindow 和 WebView 的创建耗时可以优化吗?
答:理论上可以优化,但这是一个 Native 要解决的问题。
问题 2: 页面资源的加载耗时可否消除?
答: 这个问题显然是可以解决的,资源离线就好了,但前端自己解决不了。
问题 3: 页面渲染的耗时可以优化吗?
答:这个问题前端应该可以进行优化,但可能纯前端的手段可能还不够。
在上述的文章中,笔者已对于 Weex 在正文场景下应用的优劣进行了分析,其的性能优势主要是由于渲染资源的离线,如果正文同样也基于离线缓存,那么应该可以有很不错的表现,而这一判断在与 UC 内核同学进行深度沟通后得到确认,并且内核团队也全力配合我们进行优化。
在文章最开始的时候,笔者曾对“NSR 预渲染”做了一个定义解析,即由客户端(Native 侧)实现页面结构的拼装,进而实现页面渲染的处理技术。这是 UC 内核提供的一种优化思路或方法论。
页面渲染的最短路径
从浏览器内核的角度,性能最好的页面就是——在用户访问的那一刻,页面所需的“资源”不再依赖网络而从内核的内存级缓存中获取,然后直接交给渲染引擎执行最后的处理(如下图所示),这就是理论上的完成页面渲染的最短路径了。
图片
这里就涉及资源如何不从网络获取的问题,也就是“离线”。
很显然,这里需要我们对于“离线”概念有一个正确理解,并不是说完成页面渲染的整个过程都完全不依赖于网络,它更多是一种优化的指导思想,需要开发者理解并往这个方向去优化。
当然,前端的开发者应该很容易理解这样一个公式:
UI 页面 = F 模板 (Data 数据)
通常意义上,前端开发者所说的“渲染”,其实指的是将页面分拆为数据和模板,然后根据一定的规则拼接形成完整 HTML 结构的过程。
如果这个过程在浏览器中完成,我们则称之 CSR!如果在服务端完成,那么就是 ***!
按这个思路,我们为什么不能将这个过程放在 APP 客户端上完成呢?现在的用户手机动辄好几 GB 的内存,它的运算能力可能和 10 年前的电脑差不多!
很显然,理论上没有问题。
模板和数据的预处理
从信息流场景上看,在用户访问页面之前完成页面渲染素材的准备,当用户点击访问的那一刻页面所需的资源已经处于 Ready 的状态,这样的时机是存在的。
图片
如上图所示,在用户在访问正文之前,都有大量的时间和空间给我们对页面所需的模板和数据进行预处理,只不过这些工作只能在 Native 侧进行,也就是如果要实现可以在用户触达正文前的模板和数据的组装,需要在 Native 侧建立以下的机制:
图片
除了要实现正文的模板离线机制和数据预加载外,Native 还需要处理好以下 2 件事情:
页面是动态的,即 URL 是变化的,需要实现一种页面与模板的匹配机制,这是一个“多对一”的关系;
在 APP 侧实现一个类似 *** 的本地渲染服务,而且在信息流场景下是常住的服务,因为有很多页面需要动态组装 HTML 结构。
针对以上的诉求,UC 信息流客户端进行架构的调整和重新设计,架构如下:
页面与模板的匹配,是 APP 客户端对 URL 进行拦截,然后根据 URL Path 进行规则匹配,参数则通过表达式的方式进行解析匹配(在最先上线的版本是根据 URL Path 匹配,而参与表达式则是更通用的版本),然后通过内核提供的 API 接口将主文档及其子资源设置到内核内存中,匹配流程如下:
而资源预处理服务则会包含一个类似 *** 的 JS 运行时服务。在安卓下则是由 V8 提供的 JS-Runtime,在 iOS 下则是 JS-Core,这样客户端就可以执行前端所提供的 JS-bundle,进而计算出正文的 HTML 结构。
以上,说了这么一大堆理论和过程,不过基本都是浏览器内核和客户端外壳要干的事情。这里开始,终于到了前端需要出力的时候了!
首先,需要明确一下前端离线资源该如何产出。
离线资源包的构成
这里所说的离线资源,其实就是“静态模板”,页面中相对固定的部分资源,它需要包含 HTML 模板、CSS、JS 等正文首屏依赖的核心资源。对于异步的 JS,体积比较大,我们也一并打包在离线包中。在实际的页面访问中,首屏的核心资源是直接设置到内存缓存,而异步资源则是使用 HTTP cache(磁盘缓存)。
除了前端页面渲染过程所需的资源外,针对 NSR 需求要解决以下两个问题:
HTML 模板需要一个数据占位,以便 NSR 中被替换为期望的结果;
设计并构建一个可运行在 CSR、*** 以及 NSR 场景的 JSBundle,也就三端同构的 JS。
先看结果,离线包的构成如下图所示:
资源以文件的 md5 结果为文件名,并通过 JSON 文件来描述这些资源,以便 APP 进行解析匹配,而三端同构的 js bundle 被标记为 renderjs 类型。
另外,前端模板中则在需要插入页面节点或数据的位置,与客户端约定了 注释标签作为替换占位标记。
JSON 描述文件示例如下:
{
"module": "wmmobile",
"version": "3.3.21.0",
"res": {
"545f196bb7bca3e98a8ed6c4d9211374": {
"url": "https://mparticle.uc.cn/article.html",
"type": "html"
},
"b5cdc529c83e0ca79231d08f39ab6ac3": {
"url": "https://mparticle.uc.cn/article_org.html",
"type": "html"
},
"ead6512d3d7c940ece8c916c3dc8b44f": {
"url": "https://image.uc.cn/s/uae/g/1y/infoflow/assets/js/IflowPageWemedia***.ea402c65.js",
"type": "renderjs"
},
"b7a4a62b3fb8eb99e75f323ba865db77": {
"url": "https://image.uc.cn/s/uae/g/1y/infoflow/assets/js/Article.lazy.4ff65a3e.js"
},
"ce8d98fcd6d93f5c63c34cc4f4ccf80f": {
"url": "https://image.uc.cn/s/uae/g/1y/infoflow/assets/css/IflowPageWemedia.cc645f39.css"
},
"692fd503bfcc28b3ed745d9531f4fd00": {
"url": "https://image.uc.cn/s/uae/g/1y/infoflow/assets/js/IflowPageWemedia.b88e2b3b.js"
}
}
}
这个文件以及其他离线资源的生成则需要配套的工程构建体系,因此闪开优化的过程中,前端的第一件事情是要改造前端的构建体系。有经验的同学应该都会明白,一旦改造前端项目的构建体系,意味着你可能要调整前端的架构,甚至要重构。这并不是一件简单的事情。
为什么要三端同构
在对前端架构动手前,我们得想一下动手之后的样子。
在前面笔者已交代过,图文页面除了信息流场景外,还有很多其他的场景,我们也期望能尽可能的优化,但期望代码还是同一一套,道理相信大家也应该懂的:一是没有那么多人力去维护,二则是 ROI 问题。
那么,同构是必然的,也是必须的。它们分别的作用如下图所示:
对于前端的改造,是首屏的 JS-Bundle 要同构,并且体积也要足够小。那么,前端要怎么做?
优化前的信息流前端架构
事实上,UC 信息流正文业务是由于最开始有不同技术团队分别维护,再加上数据来源不统一等原因,导致技术架构有很重的历史包袱,在 2017 年我们进行了一轮架构的优化——前端微服务,它的优缺点如下:
优点:
解决了不同类型页面相同功能的代码逻辑统一问题;
每个 SDK 微服务均可自发布,优化了功能迭代发布上线的效率;
有效降低了业务的迭代维护成本。
缺点:
SDK 微服务架构需要大量的前置依赖,对性能带来了一定负向影响;
每个 SDK 之间是串行加载,存在一定概率加载不成功的问题;
由于每个 SDK 是独立的代码仓库,单点维护成本是比较低的,但多仓库的代码管理又成为了新问题。
既然有那么多缺点,那为什么还要这么设计?
背后的原因是 UC 信息流的业务进行了组织结构的重整,而使得在大半年的时间内实际维护正文业务的只有 2 个正职同学——笔者和另一名同学,而在一年以前还是 6 人,后来自媒体业务也交接到信息流组,但维护的人力投入并未增加,维护项目迭代的总人数从原来 2 团队共 9 人,变成了 2 人 + 4 个的外包!
难为无米之炊,其实是技术方案向成本妥协的结果,虽然可以有效解决页面功能统一以及开发人力成本的问题,但却给性能和体验留下隐患。
对于闪开优化的诉求,我们期望结果是 “首屏要极致的性能”。从理论上,不使用任何框架,直接使用原生 JS 处理首屏部分,性能才是最优的。但原生 JS 由于没有框架的约束,在持续迭代过程对维护者的能力要求较高(可理解为人力成本较高),而非首屏体验则不需要追求性能上的极致,因此持续迭代的过程应该用框架来进行约束。
目前,市面上适合用于移动内容展示页面的热门框架主要是 Vue 或 React。考虑到前端团队的能力模型以及前端组所负责的其他 Weex 业务,主要采用 Rax 体系框架,对于 React 开发体系的应用已非常成熟,因此基于信息流这一特定场景,我们自研了 PureJSX 框架。
最终的前端架构如下:
正文技术架构将从下至上分成四层(在架构模式上与 UCWeex/Rax 体系基本类似):
理想与现实之间总是有差异的。架构设计可以很完美,但现实能不能落地则是另一回事。
信息流正文是一个有非常重历史包袱的项目,为了避开历史版本的兼容问题,上线的优化代码或模板是针对具备离线包能力的版本进行了切割,通过在服务端进行新旧版的模板匹配,在 UC12.0 以上版本上启用新模板,并与产品沟通后确认不再对旧版本进行新功能的迭代,只进行必要的维护。
同时,在新版本的重构中,为了达到首屏的最小化依赖,我们做了以下的事情:
在逻辑上将原来的如 Vue、Zepto、Lazyload 等基础类库剥离后,新加入的 Preact 类库在异步 chunk 中才会引入,不影响首屏;
在首屏的最小化模块中,其代码书写上仅使用兼容 ES5 的语法,保障首屏不再需要引入 Polyfill,而非首屏部分则不做限制;
重新定义首屏的功能,将原来包含 Dom 事件绑定、统计上报等逻辑全部移到非首屏的 Chunk 中。
其中,在作者的关注状态上,如果原来没有状态缓存而用户又有状态更新的,则关注区块的 UI 状态会发生变化而重绘(用户肉眼可见),我们说服了产品接受了这一点,因为这样做可以让正文不再依赖任何的前置请求和类库,进而极大地简化首屏的逻辑。
很显然,我们所采用优化策略是与场景及历史背景息息相关,同样要达到闪开的体验,其实还有其他的手段或策略,甚至实施起来更为简单。
事实上,我们也尝试了 *** 的方案,即页面结构有服务端生成,客户端把页面主文档预加载到本地并设置到内核内存缓存中,不过如果采用此方案则需要解决两个问题:
服务端算力问题此问题会延伸为机器部署、服务稳定性、可用性、QPS 压力等服务端的问题。这其实是大多数前端并不擅长的,在项目启动和执行过程,我们并没有那么多精力去处理这些问题,因此在 UC 端内我们设计了 NSR,而 *** 则作为非信息流场景下的补充方案。
预加载带来的网络峰值问题此问题的本质是成本。UC 信息流是一个超大体量的业务,如果使用 *** 渲染页面结构再通过网络加载完整页面,那么服务器和流量成本就可能会显著增加,而 NSR 则是一种成本更低的选择。
从整个优化过程来看,“闪开”优化其实是为了解决前端页面的性能问题,我们也拿到了想要的结果。但全部的优化策略中,需要 Native(外壳和内核) 来提供相应的配套机制,如果仅仅从前端开发的视角来优化,可能再怎么努力结果达不到最好。
但是,这个项目是从前端开始到前端结束,过程需要用前端的视角进行串联和策略设计,这对于前端开发者而言最大的挑战有以下几点:
充分理解浏览器的渲染原理和缓存机制;
要跳出前端范畴来思考整体的最优策略,要清楚不同端能做和该做的事项;
优化手段不局限于已有的经验,也不局限于前端已有的优化手段,敢于怀疑 *** 或 PWA 在优化场景下的适用性。
根据本文所提供的思路,我们不妨重新思考一下页面优化到底是什么?
事实上,我们所做的所有事情都是基于以下两点:
一个公式: UI 页面 = F 模板 (Data 数据)一个理论: 将模板和数据分拆,并尽可能保障在用户触达前获取,然后根据场景选择合适的组装“地点”。
基于以上的方法论,在信息流场景下我们根据业务场景而设计了 NSR,并推动了客户端和内核提供配套的处理机制,可能许多开发团队并不具备如此深度定制的能力,但这并不影响开发者重新认识和理解 Web 性能优化这件事情。
诚然,在不同场景和背景下优化的策略设计并不是一成不变的,笔者期望通过对于信息流场景的 Web 性能优化的探索和实践,为前端开发者提供一种新参考或思路。
活动推荐
当今是云原生与物联网的时代,产业进入高速发展阶段,对技术架构、操作系统以及交付模式都提出更高的要求。ArchSummit 全球架构师峰会深圳站特别开设免费技术专场,探讨在云原生时代下,从架构、软件技术、支撑平台、组织运作等方面如何进行系统性建设,以及云化 DevOps 工具链架构及设计细节和建设过程中的心得体会,阅读原文或识别下图二维码报名参会。
标签:启用 jsb 根据 数据 切割 数组 wem 代码仓库 成本
原文地址:https://blog.51cto.com/15057848/2567147