性能文章>从47%到80%,携程酒店APP流畅度优化实践>

从47%到80%,携程酒店APP流畅度优化实践转载

2年前
349912

一、导语

APP性能提升一直是研发团队永恒的主题。在进行APP性能优化实践中,除了性能技术方案本身外,还会面临两方面问题:第一,APP的性能优化,不具有持续性,往往经过一段时间优化实践,效果明显,但是随着后续需求迭代和代码变更,APP性能很难维持在一个较好的水平上;第二,APP性能改善提升,缺乏一套科学量化手段进行衡量。

引⽤管理学⼤师彼得•德鲁克的⼀句话:If you can’t measure it, you can’t improve it,如果你⽆法度量它,你就⽆法改进它。基于此,携程酒店前端APP团队进行了深入思考和探索,希望通过量化,治理,监控三方面手段,持续改善APP性能和用户体验。

87B0A4CC-17AC-4DFE-BC99-53911C704863.png

正文

二、流畅度指标定义

流畅度,简单说就是度量用户使用APP体验的一部分,它是用户快速、无阻碍使用APP的一项体验指标。主要包括三方面内容:稳、快、质。稳的含义是用户在打开具体一个页面时,没有出现白屏、崩溃、闪动等。快的含义是页面打开很快,用户在页面进行交互时,操作流畅自然。质的含义,是在浏览页面时,没有无故的弹窗拦截,打断用户的操作。如下图所示:

8FDEA6EE-6238-407D-960A-C7EC61F511F5.png


  
基于以上理论基础,APP中白屏,崩溃闪退,加载慢,卡顿,闪动,报错,都是用户在感知层面形成不流畅的因素。于是我们提出了流畅率量化指标,把用户页面PV以及用户在页面触发的二次加载次数之和,定义为流畅率的分母,也就是样本总量,如下公式:

样本量 = 页面pv+二次加载数
 
把页面慢加载/页面卡顿/图片/视频慢加载PV去重后数量,加上页面出现的崩溃,滑动卡顿,图片/视频加载失败,全局弹窗报错,输入失焦,按钮点击无效,二次加载失败,二次加载慢等异常情况之和定义为不流畅因子数。那么流畅率的公式定义为:

流畅率 = (样本量-不流畅因子数)/ 样本量
 
2.1 页面可交互加载时长

页面可交互加载时长,是页面渲染绘制时间叠加网络服务的请求响应时间,可以简单用下面公式表示:

页面可交互加载时长(TTI)= 页面本地渲染时长+服务网络加载时长

2.2 页面可交互加载时长采集原理

在我们的核心页面中,都包含了Text控件,可以通过扫描页面中特定区域内的文本来确定用户可交互时间。我们的技术栈大体上分为Flutter和Ctrip React Native,以下分别介绍加载时长采集原理。
 
2.2.1 Flutter页面可交互加载时长采集原理

在Flutter中,最终的UI树其实是由一个个独立的Element节点构成。UI从创建到渲染的大体流程如下:

根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。如下图如所示:

49F2C533-7737-45D9-AE78-8E2F24A4B377.png

所以可以从根节点开始遍历Element,直到找到扫描窗口内的Text组件且组件的内容不为空,即可判定页面TTI检测成功,Flutter提供如下接口支持Element遍历:
 
voidvisitChildElements(ElementVisitor visitor) 
 
2.2.2 Ctrip React Native页面可交互加载时长采集原理

我们知道,ReactNative最终是由Native组件来渲染的,在iOS/Android中可通过从根View从View树中递归查找Text文本控件,来获取页面内文内的内容,去掉页面顶部固定静态展示和底部静态展示区域之外,扫描到的文本数量大于1个,我们就认为页面TTI检测成功了。
 
2.3 渲染卡顿和帧率

Google对卡顿定义:界面呈现是指从应用生成帧并将其显示在屏幕上的动作。要确保用户能够流畅地与应用互动,应用呈现每帧的时间不应超过 16ms,以达到每秒 60 帧的呈现速度。如果应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧,这会导致用户感觉应用不流畅,我们将这种情况称为卡顿。
 
2.3.1 卡顿标准

判断 APP 是否出现卡顿,应该从APP类型是普通应用还是游戏应用出发,不同类型APP,对应不同的卡顿标准。针对普通类型应用,可以参考借鉴Google 的 Android Vitals 性能指标,针对游戏,可以参考借鉴腾讯的 PrefDog 性能指标。因为我们APP是普通应用,简单的介绍下Google Vitals 的卡顿定义。

GoogleVitals把卡顿分为了两类:

第一类是呈现速度缓慢:在呈现速度缓慢的帧数较多的页面,当超过 50% 的帧呈现时间超过 16ms 毫秒时,用户感官明显卡顿。

第二类是帧冻结:帧冻结的绘制耗时超过 700ms,为严重卡顿问题。

另外,要注意的是,FPS的高低和卡顿没有必然关系,帧率 FPS 高并不能反映流畅或不卡顿。比如:FPS 为 50 帧,前 200ms 渲染一帧,后 800ms 渲染 49 帧,虽然帧率50,但依然觉得非常卡顿。同时帧率 FPS 低,并不代表卡顿,比如无卡顿时均匀 FPS 为 15 帧。
 
2.3.2 卡顿量化

当了解卡顿的标准以及原理之后,可以得出结论,只有丢帧情况才能准确判断是否卡顿。

Flutter官方提供一套基于SchedulerBinding.addTimingsCallback回调实现的实时帧数据的监控。当flutter 页面有视图绘制刷新时, 系统吐出一串 FrameTiming 数据 ,FrameTiming的数据结构如下:

vsyncStart,
buildStart,
buildFinish,
rasterStart,
rasterFinish
 
vsyncStart变量表示当前帧绘制的起始时间,buildStart/buildFinish表示WidgetTree的build时间,rasterStart/rasterFinsih表示上屏的光栅化时间,那么一帧的总渲染时间,可以利用下面公式得到:

totalSpan=>rasterFinish  - syncStart

对应Google Android Vitals卡顿的标准:如果一帧totalSpan > 700ms,认为发生了帧冻结,产生了比较严重的卡顿;如果1s内,有超过30次的帧的绘制时间totalSpan> 16ms,产生了呈现速度缓慢。


三、流畅度监控方案

在流畅度监控体系中,对于不流畅感知因子,进行单项分析及挖掘,旨在在迭代优化的同时,维持或提升已有的用户体验。

监控体系的搭建,分为现状及优化方向挖掘、监控指标依赖数据补齐、**度的数据监控、指标监测预警。

1B4D8039-F732-45C4-BEE8-C3DC42295CD3.png

 
监控搭建前期会对于APP现有的性能现状进行分析,挖掘可优化的方向,初步获得优化所带来的预计收益、影响的用户数等信息。如:预计使用预加载的方式,来降低用户的慢加载率,通过各场景的不同用户操作分析,以及目前客户端及服务端技术实现的现状(酒店主服务返回报文大小统计、酒店详情纯前端渲染时间等),来确定慢加载的覆盖面、触发时机,以达到更优的效果。

接下来,针对流畅度优化的业务、技改上线的同时,补充对应的监测场景埋点,以支撑流畅度量化衡量的数据,为后续的监控及预警,奠定坚实的基石。

监控体系的核心是大盘及**度监控的铺开,大盘数据(如下图所示),能快速宏观的了解用户预订体验,明确流畅度提升的进度;而通过各种维度的数据表,即可以找到提升目标,也能监控优化效果。

在实际监控中,会针对不同的指标,设计不同的监控标准,如:慢加载、白屏、奔溃、卡顿等系统因素,除了大盘指标外,还增加了各指标影响占比、酒店主页面的报错率趋势、版本对比趋势、报错机型top分布等。

对于业务场景比较重的因素,结合业务数据进行分桶等方式的监控,如:详情页房型数量关联TTI耗时分布、单酒店crash数据等。并与AB实验系统打通,业务、技改类需求都可以在AB系统中配置流畅度观测指标,比对业务或技改需求对流畅度的指标影响,作为实验是否通过的考量指标。

FABD3C9F-0A93-4EC5-A2B5-F7488DFC1BE5.png

 

对于各项指标进行单项波动预警,做到有上升就预警、有新增就预警,做到不放过、不遗漏。如:填写页业务报错量(可订服务、提交订单、失焦错误数),除了对各类报错率趋势进行监控外,还会综合实际用户流量,区分单项业务报错的流量大小进行预警,且对拆分**度(单用户、单房型等)触发次数,便于寻找到有特性的badcase,快速定位用户遇到的问题,挖掘更多的业务优化点。
 

四、流畅度治理实践

在APP流畅度治理上,主要从页面启动加载速度,长列表卡顿治理,页面加载闪动三个方面进行了诸多优化实践,这些优化并没有涉及高大上的底层引擎优化技术,也没有复杂的数学理论基础,更没有重复造轮子。我们坚持以数据为导向,用数据驱动方案,用数据验证方案,发现问题,提出解决方案,解决问题。
 
4.1 页面加载速度优化

在页面加载速度优化上,我们从2021年8月份开始进行迭代优化至今,酒店预订流程页面的慢加载率从初始值的42.90%降低至现阶段的8.05%。

6AACC8DA-F499-42AF-A058-8D2C0443A8A5.png

在页面启动加载速度优化上,一般都会采用数据预获取方案,原理是在上一个页面提前获取服务数据,在用户跳转到当前页面时,直接从缓存获取,节省了数据的网络传输时间,达到快速展示当前页面内容的效果。目前在酒店核心预订流程,都运用了数据预加载技术,如下图所示:

04D5F5DA-EEA0-41D6-A0AE-AF7E7BB9C88B.png


结合酒店业务特点,数据预加载需要考虑几个方面问题:第一,酒店预订流程页面PV量较高,酒店列表和详情页PV在千万级别。需要考虑数据预加载的时机,避免服务的资源浪费;第二,酒店列表、详情、订单填写页都有价格信息,价格信息对用户来说是动态信息,实时都有变价可能,所以需要考虑数据预加载的缓存策略,避免因为价格的前后不一致造成用户误解。

4.2 Flutter服务通道优化

携程APP采用的私有服务协议,目前发服务的动作还是在Native代码上,而酒店的核心页面已经转到了Flutter上。通过Flutter框架提供的通道技术,Native到Flutter的数据传输通道需要对数据做一次额外的序列化及反序列化的传输,同时传输的过程比较耗时,会阻塞UI的渲染主线程,对页面的加载会造成明显的影响。我们检测到这个环节之后,和公司的框架团队一起对Flutter的底层框架进行了改造,可以实现数据流直接的透传,同时不阻塞UI主线程,性能得到了极大的提升。

优化前,通过服务返回的数据流传递到Flutter使用,整个过程要经历以下4步:

①  PB反序列化
②  Reponse到JsonString的编码
③  JsonString到Flutter通道传输
④  JsonString到Reponse的解码

整个过程链路长,数据传输量大,效率低,影响到页面加载性能,如下图所示:

D7B255F2-A31D-4F61-90DC-B38CCE3FC1F8.png

改造后,通过服务返回的数据流,直接传输到Flutter侧,在Flutter直接进行PB的反序列化,传输性能得到极大提升。

①  PB的数据流Flutter通道传输
②  PB反序列化到Reponse

整个过程链路短,数据传输量小,效率高,如下图所示:

E6000524-1969-402B-955E-11D8B7C36287.png


 
4.3 卡顿问题分析和定位

在 Flutter 中,可以利用性能图层(Performance Overlay),来分析渲染卡顿问题。如果 UI 产生了卡顿,它可以辅助我们分析并找到原因。如下图所示:

81EE71F2-6547-4C72-BFF0-3785870017C9.png

GPU线程的绘制性能情况在图表的上方,CPU UI线程的绘制情况显示在图表下方,蓝色垂线表示已渲染的帧,绿**垂线代表的是当前帧。

为了保持60Hz 刷新频率,每一帧耗时都应该小于 16ms(1/60 秒)。如果其中有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条。下图演示了应用出现渲染和绘制耗时的情况下,性能图层的展示样式:

4FC7FFCA-9017-48A4-A3B5-AC0937204943.png


 
如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码执行时间。

另外我们可以借助于AS里面的Flutter Performance工具查看Flutter页面的rendering性能问题,里面有个很有用的功能Widget rebuild stats,它统计在渲染UI的时候,各个widget rebuild数量情况,可以辅助我们很快的定位存在问题的widget,如下图:
 

FFD28289-49C1-44D2-9CA4-AA16AFEB6541.png

UI CPU线程问题定位

UI线程问题实际就是应用性能瓶颈。比如在Widget构建时,在 build 方法中使用了一些复杂运算,或是在Root  Isolate 中进行了耗时的同步操作(比如IO)。这些都会明显增加 CPU 处理时间,造成卡顿。

我们可以使用 Flutter 提供的 Performance 工具,来记录应用的执行轨迹。Performance 是一个强大的性能分析工具,能够以时间轴的方式展示 CPU 的调用栈和执行时间,去检查代码中可疑的方法调用。在点击了Flutter Performance工具栏中的“Open DevTools”按钮之后,系统会自动打开 Dart DevTools 的网页,我们就可以开始分析代码中的性能问题了。

B44A66CF-6D4C-4516-8880-FE0B2CE6F2C9.png


 
GPU问题定位

GPU 问题主要集中在底层渲染耗时上。有时候 Widget 树虽然构造起来容易,但在 GPU 线程下的渲染却很耗时。涉及 Widget 裁剪、蒙层这类多视图叠加渲染,或是由于缺少缓存导致静态图像的反复绘制,都会明显拖慢 GPU 的渲染速度可以使用性能图层提供的两项参数,负责检查多视图叠加的视图渲染开关checkerboardOffscreenLayers和负责检查缓存的图像开关checkerboardRasterCacheImages。
 
checkerboardOffscreenLayers

多视图叠加通常会用到 Canvas 里的 savaLayer 方法,这个方法在实现一些特定的效果(比如半透明)时非常有用,但由于其底层实现会在 GPU 渲染上涉及多图层的反复绘制,因此会带来较大的性能问题。对于 saveLayer方法使用情况的检查,我们只要在 MaterialApp 的初始化方法中,将 checkerboardOffscreenLayers 开关设置为 true,分析工具就会自动帮我们检测多视图叠加的情况了,使用了 saveLayer 的 Widget 会自动显示为棋盘格式,并随着页面刷新而闪烁。

不过,saveLayer 是一个较为底层的绘制方法,因此我们一般不会直接使用它,而是会通过一些功能性 Widget,在涉及需要剪切或半透明蒙层的场景中间接地使用。所以一旦遇到这种情况,我们需要思考一下是否一定要这么做,能不能通过其他方式来实现。如下图所示,因为详情头部bar用到高斯模糊,同时使用ClipRRect裁切圆角,ClipRRect会调到savelayer接口,所以该部分产生闪烁。
 

3E45D978-55BA-40E4-B5BF-4C4F0F9D6D8E.png


 
checkerboardRasterCacheImages

从资源的角度看,另一类非常消耗性能的操作是,渲染图像。这是因为图像的渲染涉及 I/O、GPU 存储,以及不同通道的数据格式转换,因此渲染过程的构建需要消耗大量资源。

为了缓解 GPU 的压力,Flutter 提供了多层次的缓存快照,这样 Widget 重建时就无需重新绘制静态图像了。与检查多视图叠加渲染的checkerboardOffscreenLayers 参数类似,Flutter 也提供了检查缓存图像的开关 checkerboardRasterCacheImages,来检测在界面重绘时频繁闪烁的图像(即没有静态缓存)。

我们可以把需要静态缓存的图像加到 RepaintBoundary 中,RepaintBoundary 可以确定 Widget 树的重绘边界,如果图像足够复杂,Flutter 引擎会自动将其缓存,避免重复刷新。当然,因为缓存资源有限,如果引擎认为图像不够复杂,也可能会忽RepaintBoundary。
 
4.4 Ctrip React Native(简称CRN)页面的优化

下图是基本的CRN页面的加载流程,各个阶段的优化之前已有文章进行过描述,如容器预加载,Bundle拆分,容器复用,框架预加载等等在容器层面做了优化。

0F5C749A-6AA7-4B1C-A8EB-D5836CC64183.png

以酒店订单填写页为例,此页面采用了CRN的架构,在已有各类容器层面和框架层面的优化之后,我们重点对页面内重绘做了治理,并将重绘治理做到了极致,主要涉及到上图中的“5. 首屏首次渲染”和“7. 首屏二次渲染”。

4.4.1 页面内Action整合

此页面采用Redux架构,前期经历了几年的粗放式开发之后,页面内的action众多(Action通过异步事件的方式触发状态管理的改变,从而达到页面重绘的目的,可以参考Redux的 Action-Reducer-Store模式)。

优化前,如下图,页面初始化/开始加载/加载中/加载完成,均触发多个action,由于action是异步的,每个数据处理模块都有一些耗时和异步,加载完成后页面可能已经刷新,此处有可能展示了未处理完成的数据,等后续action执行完成后,页面会再次刷新。

由于有数据变化,页面内元素可能会有变化,从而对用户而言,页面产生了抖动,同时也会加大JS<=>Native的通信量,页面内元素的不断变化,也会不断刷新native中的渲染树,消耗大量CPU时间,进而导致页面不流畅,耗时较长。

77DBFA49-785C-4A2E-92AD-8E6BFEBF5B3A.png

针对上述情况,我们对页面内的Action做了整合:

  • 静态数据避免使用action
  • 触发时机相同的action尽量合并
  • 非必要数据延迟加载
  • 多层action的更新进行整合

整合后,页面内的action大致如下:基本只有页面初始化,主服务返回,以及后续子服务的action了。

CEC2B471-3489-4A1F-98C3-DCFF68C3269E.png

在此过程中我们采用了redux-logger的方式来监控action,同时采用MessageQueue的方式来监控action变化触发刷新的情况,如下图:

4F0F05DD-20C0-40C9-8754-97FAD8D27EB6.png

4.4.2 控件重绘治理

为了更好的控制控件重绘的频率,我们对控件做了以下拆分:

  • 尽量的拆细组件
  • 降低单文件的复杂度
  • 组件复用更加方便
  • 依赖数据变少,状态更好管理
  • 局部更新数据不影响其他组件
  • 使用Fragments避免多层嵌套

拆分之后组件颗粒度更小,弱业务相关的采用了PureComponent,强业务组件采用Component+shouldComponentUpdate+自行比较属性是否变化来避免组件的重绘。

通过上述方式的治理,进入填写页内已明显感觉页面比较轻,主服务返回后页面立等可刷新,页面的渲染速度大幅提升。
      
重绘治理我们采用了https://github.com/welldone-software/why-did-you-render的方案来检测组件由于什么原因重绘,如下图:

F4BF2650-BC12-4D84-9BE9-41653E2A66DA.png

五、规划和总结

整个APP流畅度治理中,从流畅率从初始47%提升到目前80%,页面慢加载率从原来的45%降低到现在的8%,白屏率从1.9%降至现在的0.3%,主流程页面控件闪动基本消除,APP性能及用户体验有了较明显的提升。

回顾近半年中文酒店APP流畅度实践,整个过程艰辛,也时刻伴随着焦虑。流畅度每一点的进步都不是一蹴而就,轻易达成的。但对整个团队,收获满满,整个实践过程中,我们对flutter工程架构做了整体升级,尤其是数据传输层改造,业务层逻辑收口等;数据的预加载方案,也从1.0版本升级到2.0版本。最重要的是,整个团队形成了数据量化的思想意识和用户视角出发去优化和解决问题。

目前流畅度2.0的版本也已经落地实践,2.0将更多的不流畅感知因子加入流畅度统计,如主服务的二次加载,地图慢加载、图片及视频慢加载、图片及视频加载失败、弹窗及提示信息等,从更多系统及业务层面来提升用户的预订体验。

点赞收藏
分类:标签:
Meguro
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
2
1