预搜优化,页面 TTI 时长缩减 50%!去哪儿网酒店预订体验优化经验分享~
作者介绍
任佳,2016 年加入去哪儿网,担任前端开发工作。现负责国内酒店业务开发,目前技术上聚焦于用户体验、页面性能优化等工作。
一、背景
Qunar 大前端团队一直致力于提升 App 页面的用户体验,基于前端技术手段,提高页面的流畅度和稳定性。
国内酒店作为 Qunar 的核心业务,需要时刻关注、提升预订主流程各个页面的性能指标和用户体验。近期我们使用非关键模块延迟加载、Bundle 预加载、接口提速等方案提升核心页面的流畅度,均得到了很好的效果,但是酒店详情页还未实现页面秒开的目标,需要继续提升。
酒店详情页的功能是展示酒店的基础信息、房型报价,是为用户提供预订酒店下单服务的重要入口,而 TTI 是衡量页面秒开的重要标准。
本文从现状、优化空间、具体方案设计及优化效果等方面来讲解我们对提升酒店详情页 TTI 的思考和动作,希望能给读者一些启发。
二、什么是 TTI
TTI (Time to interactive) 是指:从页面加载开始到页面处于完全可交互状态所花费的时间。
Qunar 的 TTI 较业内更为严苛,我们的 TTI 开始时间是从路由跳转开始计算,最终到页面获取到有效数据完成渲染为止。
下图为去哪儿客户端内的 RN 页面,从路由跳转到页面可交互的时序图:
根据上述表格,以安卓机型为例,我们可以看到酒店列表和酒店详情页,接口响应时间耗费了整个 TTI 时长的 40%-50%。因此,减少接口请求时间是提高 TTI 的有效方式之一。我们对酒店列表和酒店详情页服务接口进行预请求,成功提高了页面的 TTI 指标。
三、预请求的原理
预请求的核心逻辑是,在不影响当前页面加载的情况下,提前发出下一个页面的接口请求。这样下一个页面的 TTI 中,几乎完全消除了网络请求时间的占比,达到页面【直出】的效果。预请求本质上是用空间换时间,提升用户体验。
预请求需要解决两个核心问题:
触发预请求的时机
前端发出的接口请求参数,往往与用户交互行为有关。业务逻辑越复杂的页面,实现预请求的难度就越大。如果对用户行为预判不够精准,还会导致大量无效请求,增加接口 qps,造成服务资源的浪费。
我们从用户行为埋点、点展比(=点击/展示)等数据分析,考虑将以下场景实现预请求逻辑:
预请求数据的有效性
酒店中大部分页面对数据实时性要求比较高,例如:酒店房型、房间价格对用户来说是动态信息,实时都有变化的可能,需要避免信息不一致造成用户误解和不必要的损失。因此在请求参数不变的情况下,预请求缓存的数据需要设置合理的失效时间。并且需要定义唯一标识作为缓存的 key 值,避免数据错乱。
接入预请求前后的时间线对比,如下图所示:
四、酒店详情预请求现状
1、酒店详情页的 TTI 计算口径
酒店详情页的主要业务功能是为用户展示报价,包括房型、价格、优惠等信息,帮助用户选择合适的酒店价格。因此酒店详情页的可用性是报价展示完成,我们定义 TTI 的计算口径为:
TTI = 报价渲染完成时间 - 路由跳转
2、预请求详情页报价接口逻辑
1)预请求的时机
依据酒店业务流程,约 80%的详情页打开,源自用户在列表页点击跳转触发。因此,在列表页预请求报价接口,可以在打开详情页时,尽可能的使用预请求数据,省去请求接口时间,直接渲染报价数据,减少用户等待时长,提升用户体验。
结合用户行为和转化率统计,我们设定预请求报价接口有三个时机:
列表页前 n(1-3) 项卡片渲染时
列表滚动时,元素在可视区时:用户滚动时,计算在可视区展示的列表项,预请求该列表项的报价接口数据。
点击列表卡片、优惠浮层跳转到详情页时:用户点击列表卡片时,发出预请求,将请求时间消化在转场动画中。
2)预请求结果存储
对于每一次请求,对应的参数具备唯一性,因此为保障命中预请求时,获取结果的正确性,使用请求参数作为缓存的 key 值。
在列表页预请求成功后,将返回结果以 key-value 的格式,存入缓存中。
预请求的命中率除了依赖我们预判用户下一步行为的准确性外,还受接口的响应时长的影响,如果响应时间过长,当用户点击进入详情页时,预请求的结果还未返回,页面打开时也依旧不能使用预请求的数据,无法达到预期效果。
在列表页发起预请求的流程图:
3、预请求结果命中策略
详情页打开后,先判断是否命中预请求,如果命中,从缓存中直接获取返回结果处理,渲染页面;反之,如果未命中,当下发起接口请求,请求成功后,再渲染页面。
详情页中判断预请求命中流程图:
以上是目前国内酒店在酒店详情页中接入服务预请求的方案,接入后安卓机型的详情页 TTI 降低至 1024 ms(约下降 24%)。距离我们的目标页面秒开,还有一定的差距,因此我们需要进一步寻找优化空间。
五、未达到页面直出效果的原因分析
1、预请求命中判断时机延迟
延迟主要包括两点:
1) 判断命中预请求的时机晚
酒店详情页除了报价列表外,还有其他渲染模块。当前判断命中预请求的时机与非报价模块的渲染交杂在一起,占用渲染资源,拖慢命中预请求的处理进程。
2)解析参数有耗时
上文中提到,在列表页发出报价接口预请求时,使用请求参数作为 cacheKey 存入缓存中。打开详情页后需要从 scheme 上解析参数,拼接后序列化的结果作为检查缓存命中的 key 值。
由于业务逻辑的必要性,组成参数的过程中还包含多个异步请求,拖长了在详情页获取缓存 cacheKey 的时间。
2、处理 viewModel 耗时较长
报价接口返回的数据不是可交互数据,需要前端进行预处理成 viewModel。当前的逻辑是需要对报价预处理完成后,才渲染报价列表。数据预处理时间随着报价数量的增多而增长,因此即使命中了预请求,在渲染前的数据处理仍有一定耗时。
3、缓存有效时间较短
为了避免在用户跳转到详情页时出现价格和房态变动的问题,我们对酒店房型价格和可订状态的实时性进行了考虑。
通常,用户在列表页看到的价格和可订状态可能在跳转到详情页时发生变化。如果我们在此情况下使用预请求结果,用户可能会看到错误的价格和房态,从而影响整个流程的顺畅性,甚至导致下单失败。
为了解决这个问题,我们决定将预请求结果的缓存时间设定为 30 秒。这意味着,当用户在列表页停留超过 30 秒后点击卡片跳转到详情页时,我们将不再使用预请求结果进行数据展示,而是需要重新发起接口请求。
通过用户行为分析,并与同行业类似产品进行比较,我们发现当前缓存有效时间较短。事实上,在用户停留列表页超过 30 秒后再跳转到详情页的情况存在一定比例,这对预请求的命中率产生了影响。因此,我们进行了相应的调整。
六、优化方案
针对上述原因,我们做出如下优化
1、优化预请求命中判断的时机
1)判断命中预请求的时机提前
当详情页容器初始化时,就判断是否命中预请求,前置于其他模块的处理。如果命中,报价列表在首次渲染时,就已经拿到了缓存数据。
2)优化缓存 cacheKey 的结构
为解决解析参数耗时的问题,在优化方案中,使用当次进入列表页面的唯一标识和酒店唯一标识(traceId_seq)作为 cacheKey,其中 traceId 与请求参数一一对应,完全可以替代原方案中的参数序列化做缓存的 key。
用户在列表页点击跳转详情页时,将 cacheKey 拼接到链接上,进入详情页后可直接从链接上获取 cacheKey 做预请求命中判断,省去了解析拼接参数的时间。
2、分批处理 viewModel
在优化方案中,命中预请求获取到缓存的数据后,优先渲染首屏报价,异步处理全量报价的 viewModel。首屏报价数量较少,相比原方案中预处理全量报价的 viewModel,节省了数据预处理时间。
3、增加缓存有效时长
通过实验,将缓存有效时间延长到 60s,对整体顺畅度和生单没有负影响。
优化后,在列表页发起预请求的流程图:
优化后,详情页中判断预请求命中流程图:
七、优化效果
上述优化方案实施后,酒店详情页的 TTI 时长得到大幅下降。
1、发布前后 TTI 监控对比图
2、详情页流畅度分数提升
去哪儿使用 QDD(Qunar Develop Digital) 平台,对页面的流畅度进行测量和评分。
优化后,详情页流畅度分数提升约 36 分。
未来,我们会将预请求方案应用于更多的核心页面,提升整个预订流程的用户体验。
版权声明: 本文为 InfoQ 作者【Qunar技术沙龙】的原创文章。
原文链接:【http://xie.infoq.cn/article/1a84efafba3048d0b1068a42b】。未经作者许可,禁止转载。
评论