React Native性能优化实践与技巧

Viewed 0

与原生开发相比,React Native 在页面渲染速度方面存在明显不足,例如加载慢和渲染效率低。这些问题在使用 React Native 开发跨平台应用时必须优化。那么,React Native 的性能优化应该如何进行呢?

对于许多开发者来说,这可能是一个挑战,因为除了业务开发外,对 React Native 原理的了解可能有限。根据经验,未经优化的 React Native 应用主要存在三个瓶颈。性能优化包括 JavaScript 侧和原生容器,但本文将主要从客户端角度进行探讨。

一、React Native 环境预创建

在最新的 React Native 架构中,Turbo Module(新架构下的通信方式)是按需加载,而旧框架则在初始化时加载所有 Native Modules。同时 Hermes 引擎放弃了 JIT,在启动速度方面也有明显提升。如果对 React Native 新架构感兴趣,可以参考相关文档。

抛开框架优化,在启动速度方面,我们还能做些什么呢?首先,考虑 React Native 环境预创建。在混合工程中,React Native 环境与加载页面的关系如下:独立的 React Native 载体页都有自己独立的执行环境。Native 域包括 React View 和 Native Modules;JavaScript 域包括 JavaScript 引擎、JS Modules 和业务代码;中间通信使用 Bridge 或 JSI(新版本)。

业内也有多页面复用引擎的优化,但存在一些问题,如 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;

    public void init(Context context) {
        mRNEnv = new RNEnv(context);
    }

    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) {
       buildReactInstanceManager(context);
       // 其他初始化
   }

   private void buildReactInstanceManager(Context context) {
      mReactInstanceManager = ...;
   }

   public void startLoadBundle(ReactRootView reactRootView, String moduleName, String bundleid) {
      // ...
   }
}

预创建时需注意线程同步问题。混合应用中,React Native 由应用级变为页面级使用,线程安全方面存在问题。预创建会并发创建多个环境,而内部构建存在异步处理,一些全局变量如 ViewManagersPropertyCache 中的 CLASS_PROPS_CACHE 和 EMPTY_PROPS_MAP 是非线程安全的数据结构,并发时可能引发问题。

二、异步更新

原先进入 React Native 载体页后需先下载最新 JavaScript 代码包版本,若有更新则下载并加载。过程中经历两次网络请求:检查更新和下载热更新 bundle 包。如果网络差,下载慢,等待时间较长。

针对部分特殊页面,采取异步更新策略。主要思路是在进入页面之前选择性地提前下载 JavaScript 代码包,进入载体页后优先加载缓存并渲染;然后异步检测是否有最新版本,若有则下载缓存,下次进入时生效。

在业务页面中,可对 JavaScript 代码包进行提前下载缓存。用户跳转到 React Native 页面后,检测缓存并直接渲染页面,无需等待版本号检测和下载网络接口,减少用户等待时间。渲染页面同时,异步检测代码包版本,若有新版本则更新缓存,下次生效。业务也可选择更新后提示用户刷新。

三、接口预缓存

在优化环境初始化和 bundle 加载后,React Native 页面可达到秒开级别。但页面加载后,JavaScript 业务执行中常进行网络交互请求数据,这部分也有优化空间。

具备热更新能力的 React Native 加载流程是从环境初始化到热更新,再到 JavaScript 业务代码执行,最后业务界面展示。链路长,每个步骤依赖前一步结果,热更新流程可能涉及两次网络调用。

优化点是在等待网络返回过程中,Native 利用闲置 CPU 资源。参考客户端开发中的接口数据缓存策略,在最新数据返回前先使用缓存数据渲染页面。优化后流程如下。

具体实现:打开载体页时,解析对应 bundle 缓存中的预请求接口配置数据,发起请求并缓存。

public class RNApiPreloadUtils {
    public static void preloadData(String bundleId) {
       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);
        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 官方版本未变,代码包中 React Native 源码部分不变,因此不需要每次下发,可在工程中内置。

打包时,将包拆分为两部分:Common 部分(React Native 源码)和业务代码部分(动态下载)。拆分后,Common 包内置到工程中(几百 KB 大小),业务代码包动态下载。利用 JSContext 环境,在进入载体页后先加载 Common 包,再加载业务代码包。

iOS 原生部分加载逻辑:

- (void)loadSourceForBridge:(RCTBridge *)bridge
                 onProgress:(RCTSourceLoadProgressBlock)onProgress
                 onComplete:(RCTSourceLoadBlock)loadCallback{
    if (!bridge.bundleURL) return;
    [RCTJavaScriptLoader loadCommonBundleOnComplete:^(NSError *error, RCTSource *source){
        loadCallback(error,newSource);
    }];
}

+ (void)commonBundleFinished{
    [RCTJavaScriptLoader loadBuzBundle:self.bridge.bundleURL onComplete:^(NSError *error, RCTSource *source){
        loadCallback(error,newSource);
    }];
}

+ (void)loadBuzBundle:(NSURL *)buzURL
           onComplete:(WBSourceLoadBlock)onComplete{
    [self executeSource:buzURL onComplete:^(NSError *error){
      onComplete(error);
    }];
}

五、按需加载

通过拆包方案已减少动态下载的业务代码包大小,但部分业务庞大,拆包后业务代码包仍很大,导致下载速度慢且受网络影响。

可再次拆分业务代码包为一个主包和多个子包。进入页面后优先请求主包代码资源,快速渲染首屏;用户点击模块时再下载对应子包并渲染,进一步减少加载时间。

何时需要拆分?当业务逻辑简单时无需拆分,但大型复杂项目可能需按业务拆分。例如,包含 Tab 的业务页面,如果 Tab 内容差异大,页面模版不同,可拆分业务代码包。

六、其他优化

除上述优化外,React Native 运行时优化也是一大点。旧版本运行效率痛点:JSC 引擎解释执行 JavaScript 代码效率低,引擎启动慢;JavaScript 与 Native 通信效率低,特别是批量 UI 交互。

React Native 新架构采用 JSI 通信,替换 JSBridge,无异步序列化与反序列化、无内存拷贝,可同步通信。此外,React Native 0.60 及以后版本支持 Hermes 引擎,相比 JSC,在启动速度和代码执行效率上有大幅提升。

6.1 开启 Hermes 引擎

Hermes 是轻量级 JavaScript 引擎,专为移动端 React Native 优化,可执行字节码或 JavaScript。优化体现在字节码预编译和放弃 JIT。

字节码预编译:在编译期间生成字节码,避免运行时转换时间,优化字节码提高效率。放弃 JIT:避免启动时预热影响启动时间,减少引擎大小和运行时内存消耗。但放弃 JIT 后,纯文本 JavaScript 代码执行效率降低,且字节码文件比纯文本大。

开启 Hermes 方法:参考官方文档,混合工程中以 Android 为例:

  1. 获取 hermes.aar 文件(node_modules/hermes-engine)。
  2. 将 hermes-cppruntime-release.aar 和 hermes-release.aar 放到 libs 目录,在 build.gradle 添加依赖。
  3. 设置 JavaScript 引擎为 Hermes。
  4. 运行 Hermes 编译的字节码 bundle 文件:先打包成 bundle,使用 hermesc 转换字节码,重命名文件。

6.2 引擎复用

混合应用中,React Native 由应用级变页面级使用,每个页面使用一个引擎,内存占用高且创建耗时。常见优化是引擎复用,但存在坑:创建和复用成本导致页面首次和后续进入速度不一致;多页面同时在前台时同步问题;复用容器内容会保持上一次会话全局变量,易业务逻辑错误;JavaScript 上下文隔离问题;引擎异常状态识别。

以上是 React Native 优化的常见手段,包括环境预创建、异步更新、接口预缓存、拆包、按需加载、Hermes 引擎和引擎复用。这些在实际业务中实用,同时 React Native 框架自身也在不断优化迭代。

0 Answers