随着RN技术在业务中广泛的应用,一些比较重要的功能也开始采用RN的方案来进行了,这就给RN页面的打开速度提出了更高的要求,因为打开速度是影响用户跳出率的重要原因之一。
拆包
对于RN打开速度优化,业界比较通用的方案也就是预热+拆分基础包,减少容器初始化时间和基础库加载时间。
对RN进行拆包可以依赖于官方提供的工具进行,但是官方提供的能力是JS内部的一个拆分加载,如果我们需要做容器预热,则无法使用官方的加载方案,而需要我们从客户端原本的逻辑中进行,进行多步加载。
我们需要对RN的逻辑进行改造,就需要对RN初始化逻辑有所了解。
上图是一个大致的过程,这里我们对比较关键的几个步骤进行简单的说明。
- RNBridge在实例化之后,会首先准备好JS运行线程和原生模块。
- 然后会创建一个JSExcutor,这个执行器决定了JS执行环境是客户端还是远程调试(安卓可以是自己定制的执行器,比如v8)。
- 加载源码(bundle),这个根据来源不同可能是从本地加载,也可能通过url从远端加载。
- 由于初始化JS执行器和代码是并行触发的,这里需要一个栅栏同步两者结果,之后开始将代码放入执行器执行(JS代码运行)。
- 在此之后,客户端会监听垂直同步信号(该信号的作用是在页面发生变更的时候,需要重新刷新页面)。
- 此时RootView收到JS加载完成的通知,开始触发RunApp逻辑,该逻辑就是启动前端的app注册表中对应的应用。
整个流程比较长,但是分工还是相当明确的,此次拆包改造的地方也非常明确。
上图中绿色框内就是我们此次改造的点,这里为了逻辑简单与实际需求,将加载代码设计了串行加载,如果有需要,加载过程也可以进行并发。
这里我们对加载能力进行一次抽象,加载一段代码定义为一个SourceLoader
,那么一个拆包bridge就相当于有一个加载器列表,对应于bridge上的属性就非常简单。
1 | @property (nonatomic, strong) NSArray<id<RCTBridgeSourceLoaderProtocol>> *preloadSourceLoaders; // 预加载的加载器 |
这里有一个需要注意的点是,我们需要启动一个垂直同步信号监听,为了性能考虑,需要在预热容器加载到真正视图的时候才能开启,所以这里对加载器增加一个标记,只有加载到该加载器之后才能开启监听。
经过这样的改造,我们的RN就已经支持了多包分布加载了。我们就可以把一些基础功能的js代码打包进app内部,也减少一些包大小。
容器预热
以上的分包加载并不能对加载速度有太大的影响,而真正的优化点是容器的预热,可以将很多准备工作先做了,在业务加载的时候只会触发加载业务代码与渲染页面。
受限于手机性能的局限,以及一些苹果官方的策略,我们不太可能无限制的去使用该能力,所以这里按3个方面来看预热。
预热触发时机
目前预热触发的时机主要有下面3个点
- 冷启动
- 热启动
- 容器复用之后
在这些时机触发之后,再延迟一定时间(几秒),进行创建预热实例。延迟一会的原因是这些时机大概率都是在做一些CPU密集型任务,如果此时再加入创建预热容器这种非必须的任务,反而可能影响主业务的一些性能。
预热容器销毁
目前销毁的时机主要有下面2个
- 内存警告
- 进入后台
进入后台销毁的目的主要是为了降低后台运行的内存,虽然影响不是很大,但是目前苹果对后台app策略还是比较严格的,减少一点是一点。
预热的场景化
由于RN这种业务在我们的业务中并不是主流业务,可能大部分用户都不会使用到RN,而我们对全量用户进行无差别的开启预热功能,也不是一种最优的方式。目前我们还没有能力对用户场景进行机器学习这样的智能化分析,那么这次就对一些场景做一下简单的归类:
- 3天内没有使用过RN,则认为该用户
- 3天内没有由于内存警告而销毁的记录
- 当前应用启动周期内:
- 预加载失败3次,则该周期内不再启用预加载
容器预热的关键点是在不影响用户其他体验的时候,尽可能的提高预热的命中率,目前做的一些策略都比较简单,后续如果要优化,就需要深入业务场景中。
总结
此次RN的优化分为拆包和预热两部分,各自能力独立,并分别进行AB控制,最大可能保证稳定性。后续优化将会深入业务场景去做一些优化策略。