作者:闲鱼技术-颂晨
背景
闲鱼前端页面的性能常常被人念叨,凡跳转、必跳鱼 的印象深入人心,部分页面甚至需要跳四五下才能打开,最近我们对闲鱼前端页面系统性的做了些优化,由于闲鱼前端技术栈相对多元,不同栈技术原理各不相同,优化方案也有所差异,本文主要介绍目前闲鱼占比较重的 Weex 页面的优化过程。
闲鱼 Weex 页面多以前端渲染为主,其打开过程与 Web 页面略微相近,大致分为以下几个阶段:
我们将「从开始加载(navigationStart)到屏幕首次 paint(绘制)像素内容」的这段时间称为 白屏时间(FP),将「从开始加载(navigationStart)到首屏内容全部渲染完成」的这段时间称为 首屏时间(FSP),受限于统计口径,目前 Weex 下的首屏时间是不包含图片下载及后续过程的。
优化前后
我们拿闲鱼的直播频道页和玩家频道页作为参考,通过录屏的方式看下优化前后的对比:
通过录屏分帧的方式我们统计了下这两个频道页在不同系统不同机型下的首屏时长:
可以看到,优化前 iOS、Android 主流机型上的首屏时间都要超过 2s,低端机甚至要 3-5s,优化后各机型的首屏时间均大幅下降,低端机首屏时长控制到了 2s 内,中高端机近乎直开。
拆解分析
确定优化方案前我们对现有的 Weex 页面做了拆解分析,从结果来看,以下几个因素对首屏时间的影响较大:
- Bundle 体积:不仅影响 Bundle 加载时长,同时也影响 Bundle 的解析执行耗时(低端机尤为明显)
- 首屏数据请求:页面渲染必须在首屏数据请求返回后,接口耗时直接影响首屏时间
- 首屏渲染范围:首屏渲染量直接影响渲染时长(低端机尤为明显)
优化方案
基于上面的分析调研,我们初步把优化方案定为四层:
按照预期优化效果,Weex 页面的打开过程是这样的:
体现在上述的四层结构中,主要包含以下几个优化点:
Bundle 离线
具体实现是将 Weex Bundle 以资源包为单位、以 URL 前缀为索引,通过一定的更新策略离线到客户端本地,之前的更新策略主要有 访问后安装、启动安装** 两种,对应的更新时机如下:
这套机制在容器层有统一的方案支撑,但是包命中率一直不高(25% - 55%),导致最终效果差强人意,分析后发现默认的更新策略(访问后安装)与页面回访率强相关,闲鱼的前端页面大都是频道导购型的页面,回访率天然不高,所以包命中率相对应也不会高。
本次优化主要是对更新策略进行了扩展,增加了 “闲时安装” 的更新策略:会在定时更新期间主动安装,如果安装后未使用,则会在一周之后淘汰;如果一周内使用过,则进入常规的更新淘汰机制(一个月未使用淘汰)。
在 “闲时安装” 的更新策略上线后,包命中率大幅提升(稳定后 90% 左右),页面性能也得到了显著提升:
不依赖首屏接口渲染的页面甚至可以达到「直开」:
数据预取
传统的首屏数据请求都是在 Bundle 解析完以后发起的,首屏数据返回后渲染页面,是个典型的串行过程。
本次优化中我们把这个串行的过程并行化了:
- 将首屏请求的配置序列化以后作为参数配置到了 URL 上,同时支持一些动态替换的参数(譬如经纬度、城市等参数);
- 在 navigationStart 的时候由客户端提取首屏请求配置,然后发起请求,并将结果以特定的 Hash Key(通过首屏配置生成的)作为索引存储到本地;
- 在业务层真正发起首屏请求的时候会通过 Hash Key 进行比对,命中后将数据取出来返回给业务层;
时序图如下:
特殊情况下的时序图:
具体的技术细节本文不再赘述,数据预取的优化策略上线后,首屏时间也得到了一定程度的提升,如下(iOS 侧由于各优化策略并行上线,没能做到单一变量采集性能数据,暂以 Android 侧为参考):
Bundle 离线、数据预取 的优化策略上线后,部分页面在中高端机型上逼近「直开」:
渐进式首屏
渐进式首屏解决的是「最后一公里」的问题,因为在上了「离线包」和「数据预取」的方案后,我们发现:页面首屏时间一定程度上还是受限于首屏接口请求耗时,该方案就是为了降低用户侧的白屏等待时长,具体从以下三个方面着手:
-
以接口请求配置生成的索引对接口数据进行缓存
- 当用户首次进入时,以骨架屏占位来等待业务数据加载;
- 当用户非首次进入时,会根据接口请求配置生成的索引在本地缓存中查找缓存数据,并完成首屏渲染,同时并行发送接口请求,待新数据返回后,触发页面更新,完成最终渲染;
-
低端机降级方案
为了用户体验能够更好,在此我们尝试了低端机降级优化方案。以直播频道为例:
- 只对首屏 Tab 做缓存数据占位优化
- 减少了低端机上首屏渲染展示数据量
-
图片渲染效果优化
渐进式首屏带来的一个问题是界面更新时的闪动(特别是图片占大篇幅的时候),为了优化此问题,我们将图片从加载到出现的过程改为了渐显过渡,一定程度上消除了图片闪动的生硬感。
按需渲染
渲染页面作为首屏链路中的一环,不同技术栈、不同设备环境下,在页面首屏时间中也会有不同的占比。类Weex、RN 通过前端脚本映射原生组件的技术方案,渲染路径总结起来是:渲染前端 Virtual DOM -> 映射为 Native 指令 -> 将指令传输到 Native 侧 -> Native 执行指令完成渲染。在前三个步骤上,较重的业务逻辑或不合理的代码通常会带来较长的计算、通信耗时,中低端机器上尤为明显。通过按需渲染可以有效解决这一问题。
按需渲染主要思路是通过只渲染首屏可见视图来最小化首屏渲染耗时。本次优化中,主要针对以下几个场景做了按需渲染:
- 多 Tab 情况下,对于有性能要求的非首屏 tab 页,做数据预加载、页面懒渲染处理
- 对带/不带回收机制的长列表做首屏只渲染可见条目,剩余懒渲染处理。可减少带回收机制列表的脚本计算、通信耗时,减少不带回收机制列表的全链路渲染耗时。
- 自建或使用轻量级组件替换非必要的重量级组件,如: xSlider。
优化上线后,鱼塘广场页中低端机型的首屏性能有了部分数据上的提升:
低端机上优化前端渲染阶段对比:
Bundle 瘦身
**
Bundle 体积一方面直接影响 Bundle 下载时间,另一方面也会影响 Android 端的渲染性能(耗时随 Bundle 体积增加 1-2ms/KB),我们在 Bundle 体积上的优化方案较为常规,包括:
- 通过 Webpack Bundle Analyzer 分析依赖,减少同 npm 包不同版本依赖
- 抽象公共模块,提高代码复用率
- 重构基础工具类库,支持按需加载打包
总结
闲鱼前端的性能优化暂时告一段落,优化过程中沉淀了较多的通用能力,像 Bundle 离线、数据预取、渐进式首屏等等,这些能力在后续会有更大的发挥空间,一些能力也会变得更加智能,譬如目前的数据预取是在 navigationStart 的时候发起的,这个时机已经比传统的页面加载时的时机提前了许多,但其实还可以更加提前,譬如可以在闲鱼客户端中常驻个 TaskSchedule,专门用来处理数据预取的 Task,同时可以结合用户的访问习惯做智能数据预取。
在前端性能要求越来越高的背景下,传统的 Web 加载流程已无法再满足性能优化的需要,所以出现了各种新兴容器 + 配套能力,所以下一代容器的标准形态应该是什么样的?