云音乐大前端专栏

云音乐 React Native 优化实践之拆包与预热

2020-11-12

本文作者:段家顺

背景

随着 React Native 技术在业务中广泛的应用,一些比较重要的功能也开始采用 React Native 的技术方案来实现,这就给 React Native 页面的打开速度提出了更高的要求,因为打开速度是影响用户跳出率的重要原因之一。

拆包

对于 React Native 打开速度优化,业界比较通用的方案也就是预热+拆分基础包,减少容器初始化时间和基础库加载时间。

对 React Native 进行拆包可以依赖于官方提供的工具进行,但是官方提供的能力是 JS 内部的一个拆分加载,如果我们需要做容器预热,则无法使用官方的加载方案,而需要我们从客户端原本的逻辑中进行改造为多步加载。

我们需要对 React Native 的逻辑进行改造,就需要对 React Native 初始化逻辑有所了解。

上图是一个大致的过程,这里我们对比较关键的几个步骤进行简单的说明。

  1. RCTBridge 在实例化之后,会首先准备好 JS 运行线程和原生模块。
  2. 然后会创建一个 JSExcutor ,这个执行器决定了 JS 执行环境是客户端还是远程调试(安卓可以是自己定制的执行器,比如 V8)。
  3. 加载源码( bundle ),这个根据来源不同可能是从本地加载,也可能通过 url 从远端加载。
  4. 由于初始化 JS 执行器和代码是并行触发的,这里需要一个栅栏同步两者结果,之后开始将代码放入执行器执行( JS 代码运行)。
  5. 在此之后,客户端会监听垂直同步信号(该信号的作用是在页面发生变更的时候,需要重新刷新页面)。
  6. 此时 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!