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