何宁凯,公共前端体验优化负责人
崔宇,机票主流程体验优化负责人
任佳,国内酒店体验优化负责人
去哪儿旅行APP作为我们用户流量的主要入口,其运行性能和用户体验的好坏,直接影响着用户操作的费力度,最终可能影响收益。因此我们一直很关注 App 上的用户体验,提高 App 性能,使得用户在访问页面时,更流畅,更稳定,更便捷,从而提高业务转化率。用户体验优化,首先要解决定义测量的问题。使用科学的衡量手段,评价 App 性能现状,以数据为导向,分析提升空间、并使用合理的技术方案进行改善。起初,这种测量体系由多个团队各自维护,形式多样,也没有统一的评价标准,且不具有可持续性。因此,需要建立一个统一、客观、可靠、并且可持续的可视化测量平台来解决前面的问题,于是 QDD(Qunar Develop Digital)数字化平台应运而生,QDD 平台中的用户体验模型由:流畅度、稳定性、能耗等多个指标来组成,并针对去哪儿旅行 APP 内各个页面按照这些指标进行测量。(《用户体验数字化平台落地》可了解QDD的详情介绍)基于 QDD 测量结果,我们发现与用户预订相关的核心页面在流畅度、稳定性、能耗等方面均有提升空间,于是开启了 App 的优化之路。本文主要从核心的流畅度指标来展开,把我们的优化过程中的思考、方案和成果分享给大家,来帮助大家解决类似的问题。首先我们来了解一下流畅度模型是怎么定义的,流畅度主要包含:TTI、FCP、FPS 和卡顿四个更细的指标。其中 TTI 是与用户体验相关最重要的指标。TTI (Time to interactive)是指:从页面加载开始到页面处于完全可交互状态所花费的时间。Qunar 的 TTI 较业内更为严苛,我们的 TTI 开始时间是从路由跳转开始计算,最终到页面获取到有效数据完成渲染达到可交互状态为止。解释完什么是 TTI 之后,还需要补充一个背景:去哪儿客户端里的很多核心页面是基于 React Native 来实现,这些页面按照业务划分打包成多个 QP 包。QP 包是一种用来做静态资源离线的资源包,在打开一个页面时,去哪儿客户端会先下载或者加载页面所在的 QP 包。所以我们的页面TTI除了路由相关,还需要关注React Native 所依赖的离线资源 QP 包的加载,解析,以及 RN 页面的实例化到渲染等阶段,详见下图:三、总体优化策略
通过分析前面的 TTI 时序图,我们准备从三个大的方向来着重尝试寻找优化空间,分别是:JSBundle 加载提速,页面渲染提速,服务请求优化 。
在合适的时机,进行 JSBundle 的预加载,对于实现 RN 页面的秒开,有着重要的意义。预加载有两个关键点,不能影响用户其他体验,如何深入场景提高预加载的命中率。同时,受限于移动设备的内存限制,我们也应该有合适的进行销毁时机的处理,避免产生内存问题。我们知道 Bundle 的大小,将直接影响 Bundle 的下载时长和解析时长。在进行用户体验优化前,Bundle 大小是我们的首要治理对象。通过 webpack-bundle-analyzer,我们可以得到 Bundle 的大小组成,从以下几个方面入手,着手相关优化:1. 清理无效引用或下线的代码文件。
2. 为了提升用户体验,我们会将项目中用到的静态资源图片与 Bundle 打包一起下发,这样需要我们选择合适的图片格式达到更小的Size。如以JPEG和 WEBP 替换 PNG 。
3. 选择合适的三方库,如可使用 dayjs-2kb 替换 moment.js-200kb ,大小大幅降低但功能不变。
4. 使用基础框架提供的 qrn-es6 方案优化,qrn-es6 编译后的 bundle 产物整体都是 ES6 的代码,少去了很多 babel-helper 的代码,实际数据来看,Bundle 大小最多可以减少 14% 。
考虑到机酒火主流程的 Bundle 包平均 5M 左右,为了提高首屏的显示速度,我们还需要进一步对 JSBundle 进行拆包。我们早期已将线上运行的 bundle 包拆分成了两种,框架包和业务包,业务包共用一个框架包。现基础架构已支持对业务包进一步拆包,在加载业务包时,可通过配置决定是加载全量的业务 Bundle 还是支持拆包的 RamBundle ( Random Access Modules,随机存取模块) 。我们 RamBundle 拆分为主包和子包,拆分后主包更小,加载速度更快。子包内的模块以 Inline Require 的方式按需加载,可以延迟模块或文件的加载,做到更高效的加载。线上数据显示,拆包可以提升 TTI 时间 10-15% 。再进一步配合骨架屏的使用,首屏可见时间提升 30-50% 。这里页面渲染阶段是指从创建 RN 页面到页面可交互的过程。这个阶段我们主要使用了两个优化方案:页面预加载和非核心视图延后加载。之前我们做了 JSBundle 预加载的优化,实现了打开RN页面的时候不需要先加载 JSBundle 的效果,所以一般来讲我们一个 JSBundle 包中会放多个 RN 页面。但是在打开 RN 页面的时候,我们还要做一些代码加载的工作。为了更好地解释这个问题,我们先了解一下我们的 RN 页面加载的过程。
在加载 JSBundle 阶段,我们会在执行 JSBundle 的时候注册好页面等待调用。具体流程是将 JSBundle 加载到内存中之后,然后会执行入口 js 代码,这个时候我们就会调用 AppRegistry.registerComponent 方法,将页面注册进去。一般我们在入口代码中依赖了的 js 模块都会被加载并执行,但是被注册的页面的相关代码并不会主动加载执行。为了尽快完成 JSBundle 的加载,一般我们也不会在加载 JSBundle 的时候将所有代码都预加载。而是当创建页面的时候,再开始加载并执行页面相关的代码。
从这个流程中,我们可以看到,加载相关 js 代码是可以提前到前一个环节中的。经过改造,优化后的流程如下:
这样,我们就在页面创建到首屏渲染过程中减少了加载 js 代码的时间。从页面加载完成之后,就是开始渲染、首帧绘制、页面可交互三个阶段。由于我们做了接口的预请求,而且接口返回时间一般是大于首屏渲染时间的,所以这个阶段的时间优化主要看收到接口数据以后如何优化渲染速度(下图橙色区域)。
从上图可以看到,从 loading 状态以后,就会渲染所有页面元素,这里业务复杂,渲染耗时最高,我们需要缩短主内容的 DOM 更新和视图呈现时间,让页面核心内容和用户交互功能优先可用 。1. 降低复杂度:js 侧中的 render/didmount/didupdate 仅是视图变更操作 commit 前的行为,不涉及实际的 native 侧 Flat 布局转换和渲染过程,因此这部分时间仅由业务逻辑和数据转换复杂度、DOM 树复杂度决定纯 CPU 计算类耗时,这部分可通过中大规模重构来优化,但性能优化收益较为有限。效益一般,成本偏大,风险偏高。2. 渐进式渲染,将上方紫色区域所涉及的 DOM 元素更新需求按优先级做分组,通过干预组件 DOM 更新,优化调度顺序,力图达到下图所示 DOM 更新顺序效果。如果设计得当可以预期效益显著,成本一般,低风险。
平衡效益、成本、风险要素本期选择II作为优化策略实施。基于 React Native 的 JSTImers 实现和任务调度方法原理,将渲染优先级分级为 5 个级别如下:
我们可以提供一个 HOC 高阶组件 Slugger 来切断 React 原本的父子组件的更新联动,基于调度策略在对应的执行周期上提交对干预的目标组件 Target 的 props 修改。
在非 OP_INLINE 模式下,允许提供 placeholder 属性来定制占位视图,该视图会在组件被 mount 前用于占位显示。1.选择需要进行渲染干涉的组件,依据但不仅限于下面要求:import { Slugger } from 'qunar-mobility-tweak';
const statelessWidget = _ => <Slugger
target={需要干预渲染的Component类型}
schedule={调度策略,默认为OP_NEXT_DRAWFRAME}
{...props配置给target组件的属性和事件}
placeholder={如果schedule为非OP_INLINE模式,该属性为首次渲染前的占位显示style}
/>
App 中大多数页面都是使用接口请求返回的数据渲染页面。从 TTI 时序图中可以看出,接口请求时长,直接拖慢页面 TTI 阶段的耗时。因此想要降低TTI时长,接口的响应时间也是我们可以努力的一个方向。针对服务接口响应时间的优化,首先想到的就是接口提速,降低请求数据的时长;其次是针对合适的业务场景是不是可以提前加载数据,这样需要时就可以直接使用预先加载好的数据,来达到降低 TTI 的目标;所以服务请求的优化,我们主要做了两件事:接口响应时长优化和预请求。通过分析各业务线主流页面,发现很多页面接口耗时在TTI中的占比大约在50%左右,因此接口提速有很大的优化空间,于是我们开始push对应的后端团队来优化接口时长。下面是酒店业务中某个场景的优化效果:接口链路复杂度高、不必要的串行调用服务,都会增加接口响应时长。酒店报价团队精简接口链路,进订接口耗时缩减约150ms,填单页TTI时长也相应降低。
预请求的核心逻辑是,在不影响当前页面加载的情况下,提前发出下一个页面的接口请求,将返回结果存储到缓存中。这样下一个页面的 TTI 中,几乎完全消除了网络请求时间的占比,达到页面【直出】的效果。前端发出的请求参数与用户交互有关,业务逻辑越复杂的页面,预请求的难度越大。如果对用户行为预判不够精准,还会导致大量无效请求,增加接口 qps ,造成服务资源的浪费。我们从用户行为埋点、ctr 等数据分析,考虑将以下场景实现预请求逻辑:
酒店中大部分页面对数据实时性要求比较高,例如:酒店房型、房间价格对用户来说是动态信息,实时都有变化的可能,需要避免信息不一致造成用户误解和不必要的损失。因此在请求参数不变的情况下,预请求缓存的数据需要设置合理的失效时间。
总结一下上面的三种优化方案分别带来的效果如下,根据具体页面的复杂度和实际情况,数据可能会略有波动:提升方案
| 提升效果
|
JSBundle加载优化 | TTI时长平均减少 13% 左右 |
页面渲染阶段优化 | TTI时长平均减少 15% 左右 |
服务请求优化 | TTI时长平均减少 11% 左右 |
经过半年的努力,不断探索 TTI 各阶段的优化空间,方案在一些页面得到验证后,就迅速推广到其他业务和团队,来共同优化提升我们整体的用户体验 TTI 指标;下图是某个具体的页面在上述优化后的 TTI 指标变化:(分值越高,效果越好)
这是大盘的 TTI 指标,也有了明显的提升,随着陆续优化的页面数量增多,相信大盘的整体的 TTI 还会进一步提升;
未来,我们将面临更大的挑战,除了挖掘新的提升空间之外,还要在越来越多的复杂业务迭代下,维持性能的稳定性。