# RN动态化与静态化首屏加载性能对比
# 一、前言
在 Now 上调试 RN 时只需要输入 Bundle 名称和 module name 就可以打开RN
页面进行调试,让笔者滋生了Android
动态化加载 RN 的想法;众所周知动态化需要加载资源从加载的速度上肯定是稍慢于静态化加载。作为一名前端开发,本着实践的角度,本文将探索动态化及静态化首屏加载具体差异及资源加载占整个首屏的百分比。(再已有Android
项目上加载RN
资源)
# 二、首屏加载时间指标分析
# 1. 用什么指标度量首屏加载时间
在 web 开发中,常常使用FP
、FCP
、FMP
、TTI
等性能指标来衡量一个应用的性能好坏(如下图所示);本文将采用FMP
的衡量指标去衡量 RN 页面加载性能。
要使用FMP
去衡量首屏加载时间,首先得清楚首屏加载一共有几个步骤,应用加载步骤大致分为:
- js bundle 资源下载
- RN 视图创建并启动应用
- RN 资源加载
- 初始化 JS Bridge
- RN 页面加载
所以我们可以首先将首屏耗时定义为:首屏耗时 ≈js bundle 资源下载及解压耗时+RN 视图创建耗时+RN 资源加载耗时+js bridge 及应用启动耗时+首屏视图渲染耗时。
由于 RN 视图创建耗时,RN 资源加载耗时,js bridge 及应用启动耗时为 react native 上下文初始化的一个固定开销,所以我们可以将其统称为react native 上下文初始化时间。所以最终我们暂且使用:
首屏耗时 ≈js bundle 资源下载及解压耗时+react native 上下文初始化时间+首屏视图渲染耗时
# 2. 如何计算 RN 上下文初始化时间
要了解 RN 上下文初始化时间,首先需要理解 RN 加载的整个流程,以及其中比较关键的点。下图是一张 RN 加载的整体流程图:
我们知道 RN 的页面也是依托Activity
,React Native
框架里有一个ReactActivity
,它就是我们 RN 页面的容器。ReactActivity
里有个ReactRootView
,最终渲染出来的 view 都会添加到这个ReactRootView
上。ReactRootView
调用自己的startReactApplication
()方法启动了整个 RN 页面,在启动的过程中先去创建页面上下文ReactContext
,然后再去加载、执行并将JavaScript
映射成Native Widget
,最终一个 RN 页面就显示在了用户面前。
理解了 RN 的整体加载流程,我们发现:RN 应用初始化时间 ≈AppRegistry 启动 application 的时间-createReactView 的初始化时间;
但是似乎要获取到 AppRegistry 启动 application 的时间是一件比较困难的事情,由于项目只有一个页面,我们姑且可以使用 react 初始化的时间(即 constructor 触发时间)代替启动 app 时间;而createReactView 的初始化时间时间可以使用启动 RNDynamicActivity 的时间;所以最终我们使用如下方式得到应用初始化时间:
RN 应用初始化时间 ≈React 初始化时间-启动 Activity 时间;
# 3. 如何计算首屏视图渲染耗时
关注首屏视图渲染耗时,需要理解react
框架视图渲染流程,相应的需要了解其生命周期方法。下图是一张 react 组件完整的声明周期图:
从图中可以看出React
组件生命周期分成三个阶段,在第一个阶段挂加载时应用进行初始化并进行第一次的渲染;第二阶段为组件运行及更新阶段,这里可能会再次进行组件渲染;第三阶段组件进行卸载并消亡。
对应上述生命周期方法,我们模拟 RN 页面是一个电影列表;在初始阶段首先渲染 loading 视图,并开始拉取数据。获取数据后,通过改变状态(state),触发视图的再次渲染,在屏幕绘制出视图。
结合上述分析,我们将首屏视图渲染耗时定义为如下:
首屏视图渲染耗时 ≈compontDidUpdate 结束时间 – componentDidMount 开始时间
# 三、开启探测之路
# 1. 输出耗时时间
输出耗时时间有两种方法:
- 使用
console.log
的方式,并在Android
的Logcat
中查看对应的时间; - 封装
native
层日志方法给react native
调用,将最终结果通过回调的方式展示在 RN 页面上;
本文采用第二种方式,其记录方法如下:
Native 侧手机耗时时间:
public class LogModule extends ReactContextBaseJavaModule {
// ...一些方法
@ReactMethod
public void log(String type) {
System.out.println("type = " + type);
if (RNDynamicActivity.bundleType == "static") {
switch (type) {
case "mounted":
TimeRecord.mStaticLog.componentMountedTime = new Date().getTime();
break;
case "render":
TimeRecord.mStaticLog.startRenderTime = new Date().getTime();
break;
case "constructor":
TimeRecord.mStaticLog.initTime = new Date().getTime();
break;
case "updated":
TimeRecord.mStaticLog.updatedTime = new Date().getTime();
break;
}
} else {
//...动态化时间记录
}
}
@ReactMethod
public void showDynamicTimes(Callback callback) {
boolean isDynamic = RNDynamicActivity.bundleType.equals("dynamic");
HashMap<String, Long> hashMap = new HashMap<>();
//...一些时间统计
callback.invoke(new Gson().toJson(hashMap));
}
}
React Native 侧触发:
class App extends Component {
constructor(props) {
super(props);
Log.log('constructor');
}
componentDidMount() {
this.fetchData();
Log.log('mounted');
}
componentDidUpdate() {
Log.log('updated');
}
}
# 2. 动态化加载首屏性能
React Native 如何实现动态化,详情请见笔者编写的另一篇文章已有 Android 项目如何实现动态加载 RN bundle (opens new window);
为了得到相对准确且模拟真实环境下的加载,笔者分别将 js boundle 资源存放在三处:本地 mock 服务,ak 离线包资源平台和外网私人服务器上。得出以下结论:
其对比如图所示:
# 3. 静态化首屏性能
由于资源是静态化,我们可以默认资源下载时间为 zip 资源加载时间,其对比如图所示:
# 三、总结
从静态化加载和动态化加载的真实数据我们可以得出以下两个结论:
- RN 资源初始化和视图渲染时间与是否动态化无关;
- RN 动态化稍慢于静态化加载的主要原因是:资源下载的时间;
- 对于本文 bundle 资源为 212KB,所占整个应用白屏时间约为:60%左右;
当然本次比较也有一定的局限性,比如:没有考虑 bundle 资源不同大小所占首屏时间占比等等。
其实真实的加载远不止如此简单,还有很多方面需要我我们考虑。比如:
- 离线包版本管理
- 离线包加载时机,比如用户在什么时间下载离线包,加载离线包,已经打开了当前离线包,下次打开是否更新?等
- 离线包缓存机制
- 权限管理
- 降级策略
- 等待