RN页面首屏加载性能优化

前言

大家好,想必大家都知道页面首屏加载耗时越少,跳失率越低,PV和交易额也就越高,对于开发来说这是一个关键指标,也是需要持续做功的优化点。

RN页面首屏加载流程

  1. 路由跳转:路由拦截器中可以做些预处理的逻辑。
  2. Native容器创建:该过程一般耗时很少。
  3. Bundle下载:对于大型业务来说,RN的bundle一般都在远程,加载本地包的同时校验下载远程包。下载耗时与bundle体积呈正相关。
  4. RN引擎初始化:初始化C++层的JS引擎环境,创建Java与JavaScript的双向通信通道。
  5. Bundle加载:加载bundle到JS引擎中,体积越大,加载越耗时。
  6. 组件执行:解析执行JS,render中的控件映射为Native视图,该过程耗时长短取决于页面的复杂度。
  7. 请求数据:通过接口请求真实的数据。
  8. 页面刷新:根据数据刷新页面。

在做性能优化前,注意提前打点记录各阶段耗时,为每一次的优化提供直观可信的数据支撑。

性能优化

1. 包体积缩减

主要做功阶段:Bundle下载、Bundle加载。

推荐先使用react-native-bundle-visualizer分析bundle体积构成,统计所有不符合预期的文件引入。

1.1. 常规手段

  • 整理已下线的实验和不会再使用到组件,统一下线。
  • 代码层面要注意使tree-shaking生效,避免模块整体引入。
  • 图片等资源文件放到远程。
  • bundle压缩为zip包,降低网络传输耗时。

1.2. 拆包

bundle中必然会有一些不常更新的二方或三方库,如已成熟UI组件库、react-native库等,这部分可单独放到一个bundle中,不必参与每次的发版更新。

注意点 :需要改写ReactInstanceManager加载bundle逻辑,在页面加载时将两个bundle加载到同一个JS环境中,具体实现可参考CatalystInstanceImpl#runJSBundle

2. 引擎优化

主要做功阶段:RN引擎初始化,Bundle加载,组件执行

2.1. 引擎复用

根据React官方统计,在页面创建过程中耗时主要集中在JS环境初始化阶段(JS Init + Require)。

JS环境初始化阶段可以细分为:

  • 创建JS引擎,该过程涉及加载so文件和初始化JS执行环境,建立Java和JavaScript之间的双向通信通道。
  • 注册ViewMangersNativeModules
  • 加载JSBundle到内存中。
  • 解析并初始化组件并联的JS/TS文件。

引擎复用本质就是JS执行环境的复用,可以完美的避免该过程重复执行,能够大幅缩减页面加载耗时。

示例:可以创建引擎池,为不同的bundle提供引擎复用能力。

kotlin 复制代码
object ReactInstancePool {

    private val instanceMap = mutableMapOf<String, ReactInstanceManager>()

    // 不同bundle之间引擎应该隔离,否则会相互污染
    fun getInstanceByName(bundleName: String) : ReactInstanceManager {
        // 先从缓存中根据bundleName取
        if (instanceMap.containsKey(bundleName)) {
            return instanceMap[bundleName]!!
        }
        // 注册bundleName所需的ViewManager和 
        val packages: ArrayList<ReactPackage> = PackageList(MobileApplication.instance).packages
        packages.add(object : ReactPackage {
            override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
                return mutableListOf()
            }

            override fun createViewManagers(reactContext: ReactApplicationContext): List<MySimpleViewManager> {
                return mutableListOf()
            }
        })
        // 缓存中没有,就创建一个
        val mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(MobileApplication.instance)
            .setJSBundleFile("assets://$bundleName")
            .addPackages(packages)
            .setJavaScriptExecutorFactory(HermesExecutorFactory())
            .setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
            .build()
        instanceMap[bundleName] = mReactInstanceManager
        return mReactInstanceManager
    }
}

注意点:业务在使用时有些问题需要主动规避,比如常见的全局属性问题。

tsx 复制代码
import React, { useEffect, useState } from 'react';
import { AppRegistry, Text, View, TouchableWithoutFeedback } from 'react-native';

// 只会在JS初始化时赋值为1
let num = 1;

const HelloWorld = () => {
  
  const [count, setCount] = useState(num);

  useEffect(() => {
    num = count;
  }, [count])

  return (
    <TouchableWithoutFeedback style={{flex: 1, justifyContent: 'center'}} onPress={() => {
        // 每点击一次同步到state和全局变量中
        setCount(count + 1);
    }}>
      <View style={{ flex: 1, justifyContent: 'center', backgroundColor: '#fff' }}>
        <Text style={{ fontSize: 20, textAlign: 'center' }} onPress={() => {
      setCount(count + 1);
        }}>{`Hello, World 次数:${count}`}</Text>
      </View>
    </TouchableWithoutFeedback>
  );
};

num只会在引擎初始化时被赋值为1,后续引擎复用场景使用的是上一个页面污染过的数据值。

2.2. 引擎预热

提前预判用户可能进入的下一个页面,预先创建对应的JS引擎环境。

kotlin 复制代码
fun preload(bundleName: String, curAc: Activity) {
    val tmpRootView = DebugReactRootView(curAc)
    val mReactInstanceManager = ReactInstanceManager.builder().xxx().build()
    // 预热环境:初始化引擎、加载bundle
    tmpRootView.startReactApplication(mReactInstanceManager, "", null)
    instanceMap[bundleName] = mReactInstanceManager
    tmpRootView.unmountReactApplication()
}

3. 专项优化

3.1. 包下载

  • 不常更新的bundle优先内置。
  • app启动时开始下载bundle,若担心影响首页渲染流程,可延迟到路由拦截器中。
  • 预判用户接下来可能使用的bundle,提前预下载。

3.2. 请求数据

  • 预请求:正常流程是bundle加载 → 组件执行 → 请求数据,如果Native容器能提前感知到RN要请求的接口和参数,完全可以在bundle加载前发起请求,等RN真实请求时直接使用缓存即可。可以自定义DSL接口配置文件与Bundle文件一起下发给用户。
kotlin 复制代码
{
  "method": "GET",
  "api": "/rn/test",
  "startAppVersion": "8.12.26", // 启用的版本
  "params": {
    "id": "${URL.id}", // 从页面url中获取id参数
    "lat": "#{lat}", // 调用本地api取设备纬度信息
    "sceneKey": "test" // 固定传参
  }
}
  • 降低接口耗时:推后端和算法一起降低接口耗时。

3.3. 组件执行

  • 内联引用

组件执行时会逐个解析执行import的JS文件,可以通过require延迟到实际需要该文件。

tsx 复制代码
import React, { Component } from 'react';
import { TouchableOpacity, View, Text } from 'react-native';

let VeryExpensive = null;

export default class Optimized extends Component {
  state = { needsExpensive: false };

  didPress = () => {
     // 点击时再加载 
    if (VeryExpensive == null) {
      VeryExpensive = require('./VeryExpensive').default;
    }

    this.setState(() => ({
      needsExpensive: true,
    }));
  };

  render() {
    return (
      <View style={{ marginTop: 20 }}>
        <TouchableOpacity onPress={this.didPress}>
          <Text>Load</Text>
        </TouchableOpacity>
        {this.state.needsExpensive ? <VeryExpensive /> : null}
      </View>
    );
  }
}
  • RAM bundle

如果一个bundle过大,可以将bundle拆分为多个JS文件,当真正使用时再去加载,具体可参考RAM Bundles 和内联引用优化 · React Native 中文网

tips: 如果bundle仅含单页面,此举优化效果不明显。

3.4. 视觉优化

视觉优化包含Activity弹出动画、骨架屏等效果,目的就是缓解用户情绪,使之有耐心等待更长的时间。

一个行之有效的方式是在RN bundle加载过程中先让Native容器展示缓存页面,等待RN渲染完成之后再移除Native侧的展示。

3.5. 新架构

官方已经提供了新架构,可大幅提升加载性能,推荐大家尽早升级。为何要设计新架构 · React Native 中文网

4. 最后

希望以上的优化措施能为你带来些许启发,如果你有更好的优化手段,欢迎留言一起探讨~

相关推荐
蜡笔小新星26 分钟前
Flask项目框架
开发语言·前端·经验分享·后端·python·学习·flask
*星星之火*3 小时前
【GPT入门】第5课 思维链的提出与案例
android·gpt
Fantasywt4 小时前
THREEJS 片元着色器实现更自然的呼吸灯效果
前端·javascript·着色器
EasyCVR4 小时前
EasyRTC嵌入式视频通话SDK的跨平台适配,构建web浏览器、Linux、ARM、安卓等终端的低延迟音视频通信
android·arm开发·网络协议·tcp/ip·音视频·webrtc
韩家老大4 小时前
RK Android14 在计算器内输入特定字符跳转到其他应用
android
IT、木易5 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
张拭心7 小时前
2024 总结,我的停滞与觉醒
android·前端
夜晚中的人海7 小时前
【C语言】------ 实现扫雷游戏
android·c语言·游戏
念九_ysl7 小时前
深入解析Vue3单文件组件:原理、场景与实战
前端·javascript·vue.js
Jenna的海糖7 小时前
vue3如何配置环境和打包
前端·javascript·vue.js