关于react-native:React-Native如何做性能优化

57次阅读

共计 9453 个字符,预计需要花费 24 分钟才能阅读完成。

和原生开发相比,React Native 最显著的有余就是页面的渲染速度,比方页面加载慢,渲染的效率低等。对于这些问题,都是开发中常见的问题,也是应用 React Native 开发跨平台利用时必须优化的点,由此引入一个问题,React Native 的性能优化到底应该如何做?

置信对于这个问题,大多数人第一眼看到后都是很懵逼的。因为大多数人除了业务开发之外,对于 React Native 原理性的货色都理解甚少。其实,通过咱们多年的教训,一个未经优化的 React Native 利用,从大体上讲能够分为 3 个瓶颈:

当然,RN 的性能优化包含 JavaScript 侧和原生容器的优化。不过,咱们明天咱们次要站从客户端角度进行优化。

一、React Native 环境预创立

在 最新的 React Native 架构中,Turbo Module (新架构下的通信形式) 是按需加载,而旧框架则是在初始化的时候把 Native Modules 一股脑的加载进来,同时 Hermes 引擎放弃了 JIT,在启动速度方面也有显著晋升。如果对 React Native 新架构感兴趣的同学,能够参考:React Native 新架构。

抛开这两个版本在框架方面的优化,在启动速度方面,咱们还能做些什么呢?首先,咱们看一下 React Native 环境预创立。在混合工程中,React Native 环境与加载页面的关系如下图。

从上图中能够看到,在混合利用中,独立的 React Native 载体页都有本人独立的执行环境。Native 域包含 React View、Native Modules;JavaScript 域泽包含 JavaScript 引擎、JS Modules、业务代码;两头通信应用的是 Bridge/JSI(新版本)。

当然,业内也有多个页面复用一个引擎的优化。然而多页面复用一个引擎存在一些问题,比方 JavaScript 上下文隔离、多页面渲染错乱、JavaScript 引擎不可逆异样等。而且复用的性能不稳固,思考到投入产出比、保护老本等方面,通常在混合开发中,采纳的是一个载体页一个引擎。

通常,一个 React Native 页面从加载渲染到展现大抵分为以下几步:【React Native 环境初始化】->【下载 / 加载 bundle】->【执行 JavaScript 代码】。

环境初始化这一步次要蕴含的工作包含:创立 JavaScript 引擎、Bridge、加载 Native Modules(旧版)。依据咱们的测试,初始化这一步在 Android 环境中是特地耗时的。所以,咱们想到的第一个优化点就是提前将 React Native 环境创立好,流程如下。

波及的代码如下:
RNFactory.java

public class RNFactory {
    // 单例
    private static class Holder {private static RNFactory INSTANCE = new RNFactory();
    }

    public static RNFactory getInstance() {return Holder.INSTANCE;}

    private RNFactory() {}

    private RNEnv mRNEnv;
    
    //App 启动时调用 init 办法,提前创立 RN 所需的环境
    public void init(Context context) {mRNEnv = new RNEnv(context);
    }
    
    // 获取 RN 环境对象
    public RNEnv getRNEnv(Context context) {
        RNEnv rnEnv = mRNEnv;
        mRNEnv = createRNEnv(context);
        return rnEnv;
    }
}

RNEnv.java

public class RNEnv {
   private ReactInstanceManager mReactInstanceManager;
   private ReactContext mReactContext;
   
   public RNEnv(Context context) {
       // 构建 ReactInstanceManager
       buildReactInstanceManager(context);
       // 其余初始化
       ...
   }
   
   private void buildReactInstanceManager(Context context) {
      // ...
      mReactInstanceManager = ...
   }
   
   public void startLoadBundle(ReactRootView reactRootView, String moduleName, String bundleid) {// ...}
}

在做预创立时,咱们须要留神线程同步问题。在混合利用中,React Native 由利用级变成页面级应用,所以在线程平安这方面有不少的问题,预创立时会并发创立多个 React Native 环境,而 React Native 环境外部构建存在异步解决,一些全局的变量,如 ViewManagersPropertyCache。

class ViewManagersPropertyCache {
    private static final Map<Class, Map<String, ViewManagersPropertyCache.PropSetter>> CLASS_PROPS_CACHE;
    private static final Map<String, ViewManagersPropertyCache.PropSetter> EMPTY_PROPS_MAP;

    ViewManagersPropertyCache() {}
    ...
}

外部的 CLASS_PROPS_CACHE、EMPTY_PROPS_MAP 都是非线程平安的数据结构,并发时可能会存在 Map 扩容转换问题,又比方 DynmicFromMap 也有此问题。

二、异步更新

原先咱们进入 React Native 载体页后须要先下载最新的 JavaScript 代码包版本,若有更新,就要下载最新的包并加载。在这个过程中,咱们会经验两次网络申请,即获取是否有更新,如果有下载热更新的 bundle 包。如果用户网络比拟差,下载 bundle 包就会很慢,最终等待时间也会较长。

所以咱们针对局部非凡的页面,采取了异步更新的策略。异步更新策略的次要思路为在进入页面之前选择性地提前下载 JavaScript 代码包,进入载体页后再看 JavaScript 代码包是否有缓存,如果有,咱们就优先加载缓存并渲染;而后再异步检测是否有最新版本的 JavaScript 代码包,如果有,下载到本地并进行缓存,再等下次进入载体页时失效。

上图展现了咱们关上一个 RN 页面所须要经验的一些流程。流程图中能够看出,咱们从进入载体页到渲染页面,须要两次网络申请,不论网速快还是慢,这个流程算是比拟漫长的,但在进行异步更新后,咱们的流程就会变成下图这样

在业务页面中,咱们能够对 JavaScript 代码包进行提前下载并缓存,在用户跳转到 React Native 页面后,检测是否有缓存的 JavaScript 代码包,如果有咱们就间接渲染页面。这样就不须要期待版本号检测网络接口以及下载最新包的网络接口,也不依赖于用户的网络状况,缩小了用户等待时间。

在渲染页面的同时,咱们通过异步检测 JavaScript 代码包的版本,若有新版本就进行更新并缓存,下次失效。当然,业务也能够抉择更新完最新包之后,提醒用户有新版本页面,以及是否抉择刷新并加载最新页面。

三、接口预缓存

在通过 React Native 环境初始化、bundle 加载流程进行优化后,咱们的 React Native 页面根本就能够达到秒开级别了。不过,React Native 页面加载后,进入 JavaScript 业务执行区间,大部分业务都不可避免地会进行网络交互,申请服务器数据进行渲染,这部分其实也有很大的优化空间。

首先,咱们来看下具备热更新能力的 React Native 加载流程。

能够看到,整个流程是从 React Native 环境初始化到热更新,再到 JavaScript 业务代码执行,最初到业务界面展现。链路比拟长,而且每一个步骤都依赖前一个步骤的后果。特地是热更新流程,最长可波及两次网络调用,别离是检测是否须要更新与下载最新 bundle 文件。

针对这种场景,咱们想到一个优化点,在期待网络返回的过程中,Native 能不能把闲置的 CPU 资源利用起来呢?

在纯客户端开发中,咱们常常应用接口数据缓存策略来晋升用户体验,在最新数据返回前,先应用缓存数据进行页面渲染。那么在 React Native 中,咱们也能够参考这一思路,对整个流程进行优化。

上面咱们来看一下具体如何实现。首先,当咱们关上载体页时,解析对应 bundle 缓存中的预申请接口配置数据,发动申请缓存数据,并在申请胜利之后缓存申请。

public class RNApiPreloadUtils {public static void preloadData(String bundleId) {
       // 依据 bundle id 解析对应的预申请接口配置,可存在多个接口
       List<PrefetchBean> prefetchBeans = parsePrefetchBeans(bundleId);
       // 申请接口,胜利后缓存到本地存储
       requestDatas(prefetchBeans);
    }
    
    public static String prefetchData(String url) {// 从本地缓存中,依据 url 获取对应的接口数据}
}

而后,依据 url 获取对应的缓存数据。

public class PreFetchBusinessModule extends ReactContextBaseJavaModule 
    implements ReactModuleWithSpec, TurboModule {public PreFetchBusinessModule(ReactApplicationContext reactContext) {super(reactContext.real());
    }

    @ReactMethod
    public void prefetchData(String url, Callback callback) {String data = RNApiPreloadUtils.prefetchData(url);
        // 回传数据给 JS
        WritableMap resultMap = new WritableNativeMap();
        map.putInt("code", 1);
        map.putString("data", data);
        callback.invoke(resultMap);
    }
}

接下来,就能够在 JavaScript 端调用下面的办法了,调用的代码如下:

NativeModules.PreFetchBusinessModule.prefetchData(url, (result)=>{
    // 获取到后果后,判断是否为空,不为空解析数据后渲染页面
    console.info(result);
  }
);

四、拆包

React Native 页面的 JavaScript 代码包是热更新平台依据版本号进行下发的,每次有业务改变,咱们都须要通过网络申请更新代码包。不过,只有 React Native 官网版本没有发生变化,JavaScript 代码包中 React Native 源码相干的局部是不会发生变化的,所以咱们不须要在每次业务包更新的时候都进行下发,在工程中内置一份就好了。

因而,咱们在对 JavaScript 代码进行打包的时候,须要讲包拆分成两个局部:一个是 Common 局部,也就是 React Native 源码局部;另一个是业务代码局部,也就是咱们须要动静下载的局部。

通过下面的拆分后,Common 包内置到工程中(至多为几百 kb 的大小),业务代码包进行动静下载。而后咱们利用 JSContext 环境,在进入载体页后在环境中先加载 Common 包,再加载业务代码包就能够残缺的渲染出 React Native 页面,上面是 iOS 原生局部的加载逻辑。

// 载体页
- (void)loadSourceForBridge:(RCTBridge *)bridge
                 onProgress:(RCTSourceLoadProgressBlock)onProgress
                 onComplete:(RCTSourceLoadBlock)loadCallback{if (!bridge.bundleURL) return;
    // 加载新资源
    // 开始加载 bundle,先执行 common bundle
    [RCTJavaScriptLoader loadCommonBundleOnComplete:^(NSError *error, RCTSource *source){loadCallback(error,newSource);
    }];
}

//common 执行结束
+ (void)commonBundleFinished{
    // 开始执行 buz bundle 代码
     [RCTJavaScriptLoader loadBuzBundle:self.bridge.bundleURL onComplete:^(NSError *error, RCTSource *source){loadCallback(error,newSource);
    }];
}

//RCTJavaScriptLoader.mm
+ (void)loadBuzBundle:(NSURL *)buzURL
           onComplete:(WBSourceLoadBlock)onComplete{
    // 执行 buz 包代码
    [self executeSource:buzURL onComplete:^(NSError *error){
      // 执行结束        
      onComplete(error);
    }];
}

五、按需加载

其实咱们通过后面拆包的计划,曾经缩小了动静下载的业务代码包的大小。然而还会存在局部业务十分宏大,拆包后业务代码包的大小仍然很大的状况,仍然会导致下载速度较慢,并且还会受网络状况的影响。

因而,咱们能够再次针对业务代码包进行拆分,将一个业务代码包拆分为一个主包和多个子包的形式。在进入页面后优先申请主包的 JavaScript 代码资源,可能疾速地渲染首屏页面,紧接着用户点击某一个模块时,再持续下载对应模块的代码包并进行渲染,就能再进一步缩小加载工夫。

那么,什么时候须要把业务代码包拆分成一个主包和多个子包呢?把什么模块作为主包,什么模块作为子包比拟适合呢?

其实,当业务逻辑比较简单的时候,咱们并不需要对业务代码包进行拆分,过后当业务比较复杂的时候,特地是一些大型的我的项目就有可能须要进行拆包,而拆包的逻辑,通常是依照业务进行拆分的。举个例子,咱们有一下这个蕴含 Tab 的业务页面。

能够看到,页面的首页蕴含三个 Tab,别离示意三个不同的业务模块。如果这三个 Tab 中的内容类似,咱们当然就不须要对业务代码包进行拆分了。然而如果这三个 Tab 中的内容差异化较大,页面模版齐全不雷同,咱们就能够对业务代码包进行拆分。

六、其余优化

在 React Native 挪动端的性能优化中,除了 React Native 环境创立、bundle 文件、接口数据等方面的优化外,还有一个大的优化点,就是 React Native 运行时优化。

家喻户晓,React Native 旧版本的运行效率有两大痛点:一是 JSC 引擎解释执行 JavaScript 代码效率低,引擎启动速度慢;二是 JavaScript 与 Native 通信效率低,特地是波及批量地 UI 交互更是如此。

所以,React Native 新架构采纳了 JSI 进行通信,替换了 JSBridge,无异步地序列化与反序列化操作、无内存拷贝,能够做到同步通信。

除此之外,React Native 0.60 及当前的版本开始反对 Hermes 引擎。比照 JSC 引擎,Hermes 引擎在启动速度、代码执行效率上都有大幅晋升,所以接下来咱们就来重点解说 Hermes 引擎的特点、它的优化伎俩以及如何在挪动端启用。

6.1 开启 Hermes 引擎

Facebook 在 ChainReact 2019 大会上正式推出了新一代 JavaScript 执行引擎 Hermes。Hermes 是个轻量级的 JavaScript 引擎,专门对挪动端上运行 React Native 进行了优化,Hermes 可执行字节码,也能够执行 JavaScript。

在剖析性能数据时,Facebook 团队发现 JavaScript 引擎是影响启动性能和利用包体积的重要因素。JavaScriptCore 最后是为桌面浏览器端设计,相较于桌面端,挪动端能力有太多的限度。所以,为了能从底层对挪动端进行性能优化,Facebook 团队抉择自建 JavaScript 引擎 Hermes。

根据 Chain React 大会上官网给出了 Hermes 引擎一组数据,能够看出 Hermes 的确弱小:
从页面启动到用户可操作的工夫长短(Time To Interact:TTI),从 4.3s 缩小到 2.01s;
App 的下载大小,从 41MB 缩小到 22MB;
内存占用,从 185MB 缩小到 136MB。

Hermes 的优化次要体现在字节码预编译和放弃 JIT 这两点上。首先,来看下字节码预编译。古代支流的 JavaScript 引执行一段 JavaScript 代码的大略流程是:【读取源码文件】->【解析转换成字节码】->【执行字节码】。

不过,在运行时解析源码转换字节码是一种工夫节约,所以 Hermes 抉择预编译的形式在编译期间生成字节码。这样做,一方面防止了不必要的转换工夫;另一方面,多出的工夫能够用来优化字节码,从而进步执行效率。

第二点是放弃了 JIT。为了放慢执行效率,当初支流的 JavaScript 引擎都会应用一个 JIT 编译器,在运行时通过转换成机器码的形式优化 JavaScript 代码。Faceback 团队认为 JIT 编译器次要有两个问题:
要在启动时候预热,对启动工夫有影响;
会减少引擎 size 大小和运行时内存耗费。

然而这里须要留神一点,放弃了 JIT,纯文本 JavaScript 代码执行效率会升高。放弃 JIT,是指放弃运行时 Hermes 引擎对纯文本 JavaScript 代码的编译优化。当然,Hermes 也会带来一些问题,首先就是 Hermes 编译的字节码文件比纯文本 JavaScript 文件增大不少,第二点就是执行纯文本 JavaScript 耗时长。

那咱们如何开启 Hermes 呢,除了能够参考官网文档疾速开启 Hermes,上面咱们重点看一下如何在混合工程中开启 Hermes 引擎,以 Android 为例。

1,第一步,获取 hermes.aar 文件(目录 node_modules/hermes-engine)。

2,第二步,将 hermes-cppruntime-release.aar 与 hermes-release.aar 放到工程的 libs 目录总,而后在模块的 build.gradle 中增加依赖,这两个 aar 中次要是 hermes 和 libc++_shared 的 so 文件。

dependencies {implementation(name:'hermes-cppruntime-release', ext:'aar')
    implementation(name:'hermes-release', ext:'aar')
}

3,第三步,设置 JavaScript 引擎。

ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
    .setApplication((Application) context.getApplicationContext())
    .addPackage(new MainReactPackage()) 
    .setRedBoxHandler(mExceptionHandler)
    .setUseDeveloperSupport(RNDebugSwitcher.getInstance().isDebug())
    .setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
    .setJavaScriptExecutorFactory(new HermesExecutorFactory()); // 设置为 hermes

最初,运行 hermes 编译出的字节码 bundle 文件即可。而这一步又分为了几个小步骤:
将 JavaScript 打包成 bundle 文件。

 react-native bundle --platform android --entry-file index.android.js 
--bundle-output ./bundles/index.android.bundle --assets-dest ./bundles 
--dev false

应用 hermes-engine 将 bundle 文件转换成字节码文件。下载 hermes-engine,应用 hermesc 命名进行转换。

 ./hermesc -emit-binary -out index.android.bundle.hbc 
xxx/react-native/app/bundles/index.android.bundle

最初,还须要重命名 bundle 文件。做法是,将之前 bundle 目录下的 index.android.bundle 删掉,而后将以后的 index.android.bundle.hbc 重命名为 index.android.bundle。

6.2 引擎复用

在混合利用中,React Native 由利用级的应用变更为页面级,每一个页面都应用一个 React Native 引擎 (包含 JSC/Hermes、Bridge/JSI),除了内存占用高以外,React Native 引擎的创立耗时也是比较严重的。因而 React Native 另一个常见的优化就是引擎复用优化。

以 Android 为例,React Native 引擎的间接体现就是 ReactInstanceManager,外部会初始化 React Native 相干的环境。而在混合利用中,个别会配合热更新策略进行页面加载,所以应用的是 JSC/Hermes 动静加载脚本的能力。从这个场景来看,仿佛一个引擎能够运行不同的 bundle 文件,即可达到复用的目标。引擎复用的坑也十分多,比方常见的有如下几个:

  • 创立和复用引擎的老本可能会导致不少页面,第一次进入和后续进入的速度,体现不统一,因而这类体验问题还须要专项排查并优化;
  • 在多页面同时在前台的状态下,比方首页 TAB 不同页面应用的都是 React Native 页面,会存在莫名的同步问题;
  • 复用 React Native 容器内容时,会放弃上一次会话的全局变量,容易造成业务逻辑谬误。同一个引擎加载不同 bundle,JavaScript 上下文与新加载进去的代码是否实现 100% 隔离无污染可能是未知数。同时多页面 JavaScript 上下文隔离。目前引起复用的一大坑其实来源于 JavaScript 上下文多个页面混在一起,容易出错;
  • JSC/Hermes 随时有可能产生不可逆转的异样,因而引擎保护的过程中异样状态辨认也是一个问题。

以上就是明天讲的 React Native 优化的一些常见点,包含环境预创立、异步更新、接口预缓存、拆包、按需加载、Hermes 引擎、引擎复用等。这些伎俩在理论业务中十分实用,当然 React Native 框架也在从本身上一直优化、迭代,谋求性能的更高水平。

正文完
 0