内容简介:随着 React Native 在前端业界规模性的应用越来越多,各大厂也对其渲染性能越来越看重。渲染性能的主要评判指标是FMP与TTI,在 React Native 以跨平台前端框架身份逐步替代 Native 原生界面的同时,两者的渲染性能对比也逐渐浮出水面。同时,渲染性能调优在业内已存在许多可借鉴的经验,而在项目实践的过程中,往往能体验到现实与理想的巨大差距。
一、背景
随着 React Native 在前端业界规模性的应用越来越多,各大厂也对其渲染性能越来越看重。
渲染性能的主要评判指标是FMP与TTI,在 React Native 以跨平台前端框架身份逐步替代 Native 原生界面的同时,两者的渲染性能对比也逐渐浮出水面。
同时,渲染性能调优在业内已存在许多可借鉴的经验,而在项目实践的过程中,往往能体验到现实与理想的巨大差距。
参考业内先行者的经验,针对线上项目做渲染性能优化时,往往会出现事倍功半或不尽人意的情况。
概括主要原因是以下几个方面存在问题:
1)缺少可量化的渲染性能评判标准
2)缺少可量化、可视化的优化工具
3)简单堆砌式的使用多种优化方式,容易相互抵消优化效果
4)优化方式仅局限于前端,忽略了 Native 或 Service 优化方向
本文将从理论方案、实操经验以及实用 工具 三个方面介绍携程在 React Native 渲染优化方面的经验,希望能给业内的前端伙伴提供实际的帮助与启发。
二、评判标准
评判标准是基于用户交互体感实践出的量化数据,数据达标的界面可做到近似Native 界面交互体感,其中FMP和TTI是渲染优化的两个重要指标。
2.1 FMP
用户交互中,“白屏”体感的特征指标。
“白屏”的原因简单概括如下:
- Bundle 包热更新
- 启动 React Native 容器
- 业务代码加载耗时
- 服务请求耗时
但FMP耗时并非越短越好,若业务逻辑较为复杂,一味地缩短FMP容易造成TTI耗时过久,在整体性能优化上适得其反。
2.2 TTI
用户交互中,“直出”体感的特征指标。
该指标同样经过实践,得出1.6s和1.2s两条符合良好用户体感的数据线。
- 1.6s是正常发送服务请求界面的基准
- 1.2s是采用提前发送服务请求界面的基准
与FMP不同的是,TTI耗时越短,用户“直出”的体感越好,但也需注意界面功能的完整性(如功能阉割、功能操作延迟较多等情况)。
三、优化方向
有了可量化的评判标准后,可以针对性地确认优化方向。
以携程国际机票列表页为例,如图所示从开始调整至界面渲染结束,主要分为FMP与TTI两个阶段。
理论上,减少这两个阶段的耗时,就可提升渲染性能。
下面将从客户端(Native)、前端(React Native)、服务端(Service)三个方向来作详细讲解。
3.1 客户端(Native)
3.1.1 React Native 容器
Hermes 渲染引擎切换
Facebook 发布的 Hermes 引擎,在实践中的效果优异,iOS与Android端的TTI指标有明显降低。
有兴趣的同学可看下这篇文章: Hermes引擎分析
React Native 容器热启动
当 Native 打开一个崭新的 React Native 界面时,需要经过如下步骤:
其中启动 React Native 容器至加载业务代码所消耗的时长是FMP指标的关键因素。
而容器热启动的意义在于将界面加载过程中的必经流程提前运行,加快界面渲染的速度。
通常,当有多个界面采用流式加载的方式时,再前一个界面调用 Native API 提前启动下一个界面所需的 React Native 容器。
React Native 容器复用
当多个界面采用流式加载,往往会存在ABAB 式的用户流水。
由于A界面在打开B界面的时候,是作为一个容器被B界面遮罩,并没有被关闭,所以A界面只被打开了一次;而B界面在这用户流水过程中实际被打开了两次,即返回A界面时,B界面的容器就被销毁了,同时其中的 React Native 容器也被销毁。
基于上述场景,可以发现优化点在于容器及其中的 React Native 容器内容可以被缓存,便于下一次进入时可以被复用。
这里需要注意两个风险点:
1)过多的容器及其中的 React Native 容器内容被缓存时,容易造成内存溢出,从而引起 App Crash;
2)复用 React Native 容器内容时,会保持上一次会话的全局变量,容易造成业务逻辑错误。
3.1.2 Bundle
Bundle 预下载
在非业务型界面中,提前下载之后可能会被打开的业务界面 Bundle(若有更新增量存在)。
该方案仅对 Bundle 提前进行下载操作,并不会进行增量文件加压及更新。
使用该方案后,会面临如下的问题:
1)App中的界面数量往往比较多,全部使用 Bundle 预下载可能会造成网络下载列队阻塞,影响正常使用。
2)Bundle 存在下载失败的概率,会丧失预下载的想要达到的效果。
针对上述可能出现的问题,需要进行深度优化:
1)预下载的时机需要符合如下几个条件:
- 利用底包优势,以 Native 实现的界面
- 业务改动频率较低
- 具备一定停留度的界面
2)采取优先级异步多线程下载策略,按不同维度设定优先级,如 Bundle 使用率。
3)重试机制,类似 setInterval 轮询增量更新列表
Bundle预加载
在 React Native 容器热启动之前,解压 Bundle 文件并更新。
通常配合 React Native 容器热启动和 Bundle 预下载使用。
Bundle 的加载完成了下述3件工作:
1)更新Bundle文件
2)编译JS代码
3)执行JS代码
随着 React Native 容器采用 Hermes 引擎,Bundle 被打包为单个文件,相比使用 JSCore 被打包成多个文件来看:
1)更新 Bundle 文件阶段,单文件的更新速率优于多文件
2)编译JS代码阶段,单文件减少了多个文件加载耗时
3.1.3 Native To React Native API
Sync 同步
React Native 与 Native 之间采用异步通信机制,当线程繁忙时,会产生阻塞和等待。
另,在首屏渲染过程中,内存获取数据比较慢的场景也会出现,耗时可能高达200ms。
解决上述问题,主要有以下几个方向:
- 对内存读写数据类 API
- Sync API 耗时可控在毫秒级
- Chrome Dev 不支持 Sync,需特殊处理
- 有利于解决阻塞依赖 Native 异步接口调用的场景
此时,使用 Sync 同步方案显得可行,可解决如下场景:
- 获取 ABTesting 实验号
- 获取本地 Storage 内容
- 获取功能开关列表
- 获取屏幕 Size
- SOTPCookie
3.2 前端(React Native)
3.2.1 Bundle 瘦身
Bundle 中存在几种文件类型,针对不同类型选择不同的优化方案:
- 代码字符串
- Iconfont 字符串
- 图片文件
代码字符串
冗余代码是代码 Size 的主要问题,而冗余代码的产生主要源自于四个方面:
- 已下线的需求代码
- 已结项的实验代码
- NPM 冗余调用
- 缺乏抽象的重复代码
解决方案:
- 整理已下线需求,删除相应代码及库文件
- 使用组件库及方法库,减少重复代码
- 抽象可复用的组件,使用高阶组件
图片文件
在打包压缩过程中,图片文件的压缩比极低,越大的图片占用的 Bundle Size 越大。
解决方案:
- 较大的图片在保证清晰度的前提下压缩后打包
- 视业务场景使用网络图片替代
- 较小图片可以使用 IconFont 替代
3.2.2 模块
LazyRequire
在编译过程中,import会被编译成 require,require 所完成的功能是读取JavaScript 模块并执行。
而大模块的执行会耗费较多时间,使得界面加载速度变慢。
因此,优化的方向是当模块被需要才加载。但 React Native 提供的标准 require 目前并不支持动态加载。
需要修改 React Native 源码的打包功能,使其支持动态加载功能,并提供出对应的 API 来供业务方实现。
使用示例如下:
import {lazyRequire} from 'react'; let moduleA = lazyRequire('../src/ModuleA');
动态加载
使用 import 语句导入模块时,会自动执行所加载的模块。而当使用组件库或公共方法库的时候,往往并不希望如此。
假设 Common.js 文件为公共方法库
import A from './A'; import B from './B'; import C from './C'; export { A, B, C }
此时若希望只引用 Common.js 中的A模块,即
import {A} from './Common.js';
但实际B和C模块代码也被执行了。
为了使程序能如你所愿的仅执行A模块,需要使用属性 getter 动态 require 的方式来修改 Common.js 文件。
const Common = { get A(){ const module = require('./A'); return (module && module.__esModule)? module.default:module; } get B(){ const module = require('./B'); return (module && module.__esModule)? module.default:module; } get C(){ const module = require('./C'); return (module && module.__esModule)? module.default:module; } } module.exports = Common;
这样在使用到A模块的时候才会执行 require(‘./A’).default,并不会加载B和C。
至此,使用该方式导出模块可以减少引用模块时的无效加载数量,达到优化渲染速度的目的。
3.2.3 渲染方式
骨架屏/呼吸态
骨架屏是有效减少用户体感“白屏”的有效措施,通常在骨架屏完成耗时较长的关键性任务,如核心服务请求、重要异步回调等。
同时,骨架屏也是缩短 FMP 标准的重要方法,主要方式:
- 减少加载骨架屏之前的非必要模块引用
- 核心服务请求参数的拼接可放在骨架屏渲染之前完成
- 骨架屏自身的渲染结构足够简单
分批次渲染
分批次的概念主要运用在列表型界面或内容型界面。
顾名思义,是将界面需展示的内容,分成不同阶段/批次进行渲染,阶段/批次的数量根据业务自身情况而定,往往以覆盖满屏幕的主要区域为宜。
该方案对提升TTI有较大作用,可数量级的减少渲染内容,从而降低渲染耗时。
渐进式渲染
React Native 渲染的本质是将 JSX 构建的虚拟 DOM 树通过 Native Render 的方式绘制界面内容。
虚拟 DOM 树结构越复杂,Native Render 所需绘制的时间也越长。
从这个特性出发,可以通过降低虚拟 DOM 树结构的复杂度来减少渲染耗时,用尽可能短的时间到达 TTI 阶段。
降低虚拟 DOM 树结构复杂度的底线是最低程度得保证业务功能的完整性,而在其渲染完成后(达到TTI阶段),通过 setState 去更新渲染完整的虚拟 DOM 树结构即可。
下面两幅图在渲染过程中采用了渐进式渲染,可观察航空公司部分:
延迟渲染
界面在相对复杂的情况下,渲染的模块会比较多,渲染的耗时也会随着需要渲染的模块数水涨船高。
对待渲染的模块区分核心和非核心,或者区分模块需渲染的轻重缓急,优先渲染核心/重要的模块,符合界面基本交互功能(达到TTI阶段),再渲染非核心/次要模块来完成整个界面的渲染工作。
按需渲染
界面中不可避免的会存在一些浮层或者二级界面,下面统称为次级界面。
这次次级界面在TTI阶段前,大部分是不需要进行渲染的,可以配合 LazyRequire 的方式完成。
预渲染
空间换时间的经典方案。
假设存在流式界面A -> B,若能在A界面时能够提前渲染B界面的话,理论上可以做到在打开B界面时做到“直出”效果。
若要做到上述方案,需要结合多个优化方案,这里只分析“预”的实现方式。
在A界面时,通过 Native API 热启动一个新的 React Native 容器,同时在新容器内预加载B界面的 Bundle 并执行。
当从A界面进入B界面时,由于B界面已经完成/正在渲染,B界面可达到“直出”效果。
优化结构
虚拟 DOM 树的结构越复杂,所需消耗的渲染时长也就越久,也就越晚到达 TTI 阶段。
首先,通过工具去观察虚拟 DOM 树结构的深度和广度,使用渐进式渲染方案减少深度,同时也使用分批次渲染方案减少广度。
其次,由于研发过程属于 TeamWork,一个结构合理的 UI 组件库可以大幅减少优化结构所需的工作量。
3.3 Service
优先发送服务请求
从进入界面到渲染界面完成(即TTI阶段)需要经过许多代码逻辑阶段,研发人员需要理清这些逻辑阶段的依赖关系,并做出优先级的策略。
为了更快的将服务请求发送出去,利用等待服务返回数据的时间差去运行其他渲染所需的逻辑,待服务数据返回后再去渲染界面。
但需要注意的是,若服务返回时间较长,可能会子执行完其他逻辑时进入 render阶段,当服务返回数据后再次 render,造成 TTI 阶段耗时有所延长。
解决方案是采用服务预搜索后,使用同步请求服务数据的方式来避免重复/无效 render。
按需异步获取数据
类似按需渲染的场景,同一个界面需要请求的服务个数往往不止一个,除了渲染界面主要模块所必须的核心服务外,其他次要模块的服务请求可以放在 TTI 阶段后请求。
图中红色部分的模块,在渲染的界面中并不属于核心模块,可以采取延迟按需请求的方式获取数据后再进行渲染。
服务预搜索
除静态界面外,几乎所有 CSR 界面都需要在渲染过程中发送服务请求,再根据服务请求返回的内容渲染界面。
等待服务请求响应的时长将直接拖慢到达 TTI 阶段的耗时,而提前发送服务请求是否可行?
前端在发送服务请求前往往需要拼接较多的请求参数,这些参数中存在很多变量,而变量的来源有许多是来自于用户交互。
正因为这样的场景较多,提前发送服务请求的难度也陡然上升。同时,也会给服务端带来请求数量成倍增加的副作用。
处理的方式有以下几种,可根据业务形态的不同进行选择或组合:
- 区分业务不同场景,针对大部分场景做提前请求服务的操作
- 需要依据多个用户交互结果作为请求参数的场景,可配合 BI 用户模型做到较精准的提前请求
在提前发送请求服务后,在进入下一界面时,代码逻辑仍然会正常发送服务请求,这里需要做好网络缓存。具体操作方式如下:
- 请求服务时,根据请求的 url 和参数通过 Hash 生成一个唯一的 Key
- 请求返回时,将返回的数据存入本地
- 在一定时间内,发送相同 url 和参数的请求,都会匹配已生成的 Key,从本地拿取数据返回,而不进行真实的网络请求
四、实践工具
每个项目/界面的业务逻辑不同,从而代码逻辑也不相同。显然在优化不同界面时,采用的优化方案也不同。
那么,在优化界面过程中该如何选取适合的优化方案,显得尤为重要,而这个过程中,经验并不能起到决定性的作用。
需要借助线上和线下两方面的工具来完成性能分析工作后,再依据经验选择合适的优化方案。
4.1 Offline
借助 Chrome Devtool Performance,可以分析运行时的性能表现,主要借助以下两种方法采样性能数据。
- 调试环境:通用的 Web 性能分析方案,打开 React Native 调试功能–>运行项目–>采样数据。
- 真机环境:在测试环境中修改 React Native 代码,模拟 Profile 数据结构生成埋点数据。
以上两种方法存在部分差异:
- 调试环境:采样数据来自于模拟器,数据的真实性存在偏差,多用于快速试验优化方案效果。
- 真机环境:采样数据来自于真实机型,数据的真实性较为可靠,多用于验证优化方案效果,以及针对特殊机型验证优化效果。
两种方式采样到的性能数据,分为 Timing 和 Console 两种。
4.1.1 Timing
作用是分析视图组件渲染顺序与耗时,如下图使用 Timing 火焰图,在视图渲染层面分析性能:
- 组件渲染顺序与耗时:“火焰模块”的长度标识组件渲染耗时(包括其子组件),至上而下可以分析组件的渲染顺序。
- 组件间渲染空闲时间:通过两个“火焰模块”之间结构,分析各模块组件之间的渲染顺序,其中空白部分表示组件渲染空闲的耗时。
4.1.2 Console
作用是通过代码执行层面分析性能,如下图使用 Console 时序图分析性能:
- 代码块执行耗时:单个模块表示该代码块执行的总耗时,可以更直观的分析代码块的执行顺序。
- 异步与同步代码之间的关系:简单的埋点计时并不能精确计算异步耗时,而模块与模块之间的叠加关系,可以直观分析异步代码的执行耗时。
4.2 Online
业务代码在上线后,存在许多环境因素,如网络情况、机型覆盖面、OS系统、Bundle 更新及解压等。
性能优化的目的是让用户切实提升使用 App 的感受,线上性能数据采样就成了重要的试金石。
线上性能数据采样主要记录的是界面渲染的 TTI 和 FMP 耗时点,采样的方式主要采用屏幕像素检测,检测用户访问的界面屏幕渲染出像素点的耗时。
采集到 FMP 和 TTI 数据之后,根据 App 版本、OS 系统类型、时间周期等维度进行拆解,绘制出对应的性能波形图和P90覆盖情况。
具体可见文章 《携程无线APM升级实践》 。
五、小结
渲染性能已然成为大前端工程师必须面对的一道课题。本文通过工具及方案的介绍,将前端优化的视野打开,更加系统得看待渲染性能问题。
叠加使用各种优化方案在优化渲染性能方面具备一定的普适性,部分优化理论同样也适用于 H5 与 Native 平台。
【作者简介】
佳璐,前端开发专家,关注前端框架、性能、质量、效率和新技术。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 通过分析 WPF 的渲染脏区优化渲染性能
- 页面渲染:性能分析
- 【Android杂谈】性能优化:渲染
- 从渲染原理谈前端性能优化
- 通过共享内存优化 Flutter 外接纹理的渲染性能,实时渲染不是梦
- OpenGL* 性能提示:相比图像,纹理具有更出色的渲染性能
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出Struts 2
Budi Kuniawan / 杨涛、王建桥、杨晓云 / 人民邮电出版社 / 2009-04 / 59.00元
本书是广受赞誉的Struts 2优秀教程,它全面而深入地阐述了Struts 2的各个特性,并指导开发人员如何根据遇到的问题对症下药,选择使用最合适的特性。作者处处从实战出发,在丰富的示例中直观地探讨了许多实用的技术,如数据类型转换、文件上传和下载、提高Struts 2应用的安全性、调试与性能分析、FreeMarker、Velocity、Ajax,等等。跟随作者一道深入Struts 2,聆听大量来之......一起来看看 《深入浅出Struts 2》 这本书的介绍吧!