前言
大家好,想必大家都知道页面首屏加载耗时越少,跳失率越低,PV和交易额也就越高,对于开发来说这是一个关键指标,也是需要持续做功的优化点。
RN页面首屏加载流程
- 路由跳转:路由拦截器中可以做些预处理的逻辑。
- Native容器创建:该过程一般耗时很少。
- Bundle下载:对于大型业务来说,RN的bundle一般都在远程,加载本地包的同时校验下载远程包。下载耗时与bundle体积呈正相关。
- RN引擎初始化:初始化C++层的JS引擎环境,创建Java与JavaScript的双向通信通道。
- Bundle加载:加载bundle到JS引擎中,体积越大,加载越耗时。
- 组件执行:解析执行JS,render中的控件映射为Native视图,该过程耗时长短取决于页面的复杂度。
- 请求数据:通过接口请求真实的数据。
- 页面刷新:根据数据刷新页面。
在做性能优化前,注意提前打点记录各阶段耗时,为每一次的优化提供直观可信的数据支撑。
性能优化
1. 包体积缩减
主要做功阶段:Bundle下载、Bundle加载。
推荐先使用react-native-bundle-visualizer分析bundle体积构成,统计所有不符合预期的文件引入。
1.1. 常规手段
- 整理已下线的实验和不会再使用到组件,统一下线。
- 代码层面要注意使
tree-shaking
生效,避免模块整体引入。 - 图片等资源文件放到远程。
- bundle压缩为zip包,降低网络传输耗时。
1.2. 拆包
bundle中必然会有一些不常更新的二方或三方库,如已成熟UI组件库、react-native库等,这部分可单独放到一个bundle中,不必参与每次的发版更新。
注意点 :需要改写ReactInstanceManager
加载bundle逻辑,在页面加载时将两个bundle加载到同一个JS环境中,具体实现可参考CatalystInstanceImpl#runJSBundle
。
2. 引擎优化
主要做功阶段:RN引擎初始化,Bundle加载,组件执行
2.1. 引擎复用
根据React官方统计,在页面创建过程中耗时主要集中在JS环境初始化阶段(JS Init + Require)。
JS环境初始化阶段可以细分为:
- 创建JS引擎,该过程涉及加载so文件和初始化JS执行环境,建立Java和JavaScript之间的双向通信通道。
- 注册
ViewMangers
、NativeModules
。 - 加载JSBundle到内存中。
- 解析并初始化组件并联的JS/TS文件。
引擎复用本质就是JS执行环境的复用,可以完美的避免该过程重复执行,能够大幅缩减页面加载耗时。
示例:可以创建引擎池,为不同的bundle提供引擎复用能力。
kotlin
object ReactInstancePool {
private val instanceMap = mutableMapOf<String, ReactInstanceManager>()
// 不同bundle之间引擎应该隔离,否则会相互污染
fun getInstanceByName(bundleName: String) : ReactInstanceManager {
// 先从缓存中根据bundleName取
if (instanceMap.containsKey(bundleName)) {
return instanceMap[bundleName]!!
}
// 注册bundleName所需的ViewManager和
val packages: ArrayList<ReactPackage> = PackageList(MobileApplication.instance).packages
packages.add(object : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
return mutableListOf()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<MySimpleViewManager> {
return mutableListOf()
}
})
// 缓存中没有,就创建一个
val mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(MobileApplication.instance)
.setJSBundleFile("assets://$bundleName")
.addPackages(packages)
.setJavaScriptExecutorFactory(HermesExecutorFactory())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
.build()
instanceMap[bundleName] = mReactInstanceManager
return mReactInstanceManager
}
}
注意点:业务在使用时有些问题需要主动规避,比如常见的全局属性问题。
tsx
import React, { useEffect, useState } from 'react';
import { AppRegistry, Text, View, TouchableWithoutFeedback } from 'react-native';
// 只会在JS初始化时赋值为1
let num = 1;
const HelloWorld = () => {
const [count, setCount] = useState(num);
useEffect(() => {
num = count;
}, [count])
return (
<TouchableWithoutFeedback style={{flex: 1, justifyContent: 'center'}} onPress={() => {
// 每点击一次同步到state和全局变量中
setCount(count + 1);
}}>
<View style={{ flex: 1, justifyContent: 'center', backgroundColor: '#fff' }}>
<Text style={{ fontSize: 20, textAlign: 'center' }} onPress={() => {
setCount(count + 1);
}}>{`Hello, World 次数:${count}`}</Text>
</View>
</TouchableWithoutFeedback>
);
};
num只会在引擎初始化时被赋值为1,后续引擎复用场景使用的是上一个页面污染过的数据值。
2.2. 引擎预热
提前预判用户可能进入的下一个页面,预先创建对应的JS引擎环境。
kotlin
fun preload(bundleName: String, curAc: Activity) {
val tmpRootView = DebugReactRootView(curAc)
val mReactInstanceManager = ReactInstanceManager.builder().xxx().build()
// 预热环境:初始化引擎、加载bundle
tmpRootView.startReactApplication(mReactInstanceManager, "", null)
instanceMap[bundleName] = mReactInstanceManager
tmpRootView.unmountReactApplication()
}
3. 专项优化
3.1. 包下载
- 不常更新的bundle优先内置。
- app启动时开始下载bundle,若担心影响首页渲染流程,可延迟到路由拦截器中。
- 预判用户接下来可能使用的bundle,提前预下载。
3.2. 请求数据
- 预请求:正常流程是bundle加载 → 组件执行 → 请求数据,如果Native容器能提前感知到RN要请求的接口和参数,完全可以在bundle加载前发起请求,等RN真实请求时直接使用缓存即可。可以自定义DSL接口配置文件与Bundle文件一起下发给用户。
kotlin
{
"method": "GET",
"api": "/rn/test",
"startAppVersion": "8.12.26", // 启用的版本
"params": {
"id": "${URL.id}", // 从页面url中获取id参数
"lat": "#{lat}", // 调用本地api取设备纬度信息
"sceneKey": "test" // 固定传参
}
}
- 降低接口耗时:推后端和算法一起降低接口耗时。
3.3. 组件执行
- 内联引用
组件执行时会逐个解析执行import的JS文件,可以通过require
延迟到实际需要该文件。
tsx
import React, { Component } from 'react';
import { TouchableOpacity, View, Text } from 'react-native';
let VeryExpensive = null;
export default class Optimized extends Component {
state = { needsExpensive: false };
didPress = () => {
// 点击时再加载
if (VeryExpensive == null) {
VeryExpensive = require('./VeryExpensive').default;
}
this.setState(() => ({
needsExpensive: true,
}));
};
render() {
return (
<View style={{ marginTop: 20 }}>
<TouchableOpacity onPress={this.didPress}>
<Text>Load</Text>
</TouchableOpacity>
{this.state.needsExpensive ? <VeryExpensive /> : null}
</View>
);
}
}
- RAM bundle
如果一个bundle过大,可以将bundle拆分为多个JS文件,当真正使用时再去加载,具体可参考RAM Bundles 和内联引用优化 · React Native 中文网
tips: 如果bundle仅含单页面,此举优化效果不明显。
3.4. 视觉优化
视觉优化包含Activity弹出动画、骨架屏等效果,目的就是缓解用户情绪,使之有耐心等待更长的时间。
一个行之有效的方式是在RN bundle加载过程中先让Native容器展示缓存页面,等待RN渲染完成之后再移除Native侧的展示。
3.5. 新架构
官方已经提供了新架构,可大幅提升加载性能,推荐大家尽早升级。为何要设计新架构 · React Native 中文网
4. 最后
希望以上的优化措施能为你带来些许启发,如果你有更好的优化手段,欢迎留言一起探讨~