技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# 在腾讯我们是怎么做rn的性能优化的

这篇文章的标题可能有点大,作者的本意是想给大家分享一下在腾讯的这段时间从事 react-native 相关的工作,是如何对 react-native(下文称 rn)进行性能优化的,也许这是你看到的最全的一篇 rn 性能优化相关的文章.

由于笔者后续会较少的参与 rn 相关的工作,这篇文章也算是自己对这么久以来 rn 工作的总结吧,也希望对部分正在路上的同学有所启发和帮助.

# 一、前言

rn 官方的默认推荐方式是大家使用 rn 开发并作为独立的 app 使用,但是对于较大体量的 app 考虑到框架迁移的成本以及对技术保持怀疑的态度 都不会直接使用 rn 开发一个独立的 app;反而更多的是将 rn 作为内部的一些独立模块/页面.

熟悉 rn 的同学应该都清楚,想要 rn 页面在 app 中启动需要内置其一堆底层框架,或者 rn 的 jsbundle; 而内置 rn 的 jsbundle 又分为两种,其优缺点也都较为明显;

  1. jsbundle 全量内置

    • 优点: 启动速度快
    • 缺点: 每次更新 rn 页面都需要跟随 app 发版本,特别是针对页面出现重大 bug 的情况无法做到及时的更新;
  2. jsbundle 部分内置

    • 优点: 内置通用的 jsbundle,动态下发业务 bundle;可以动态更新页面
    • 缺点: 加载速度相比内置会多一个下载时间,启动速度会有少许的缓慢;

看了上面两种 jsbundle 我们团队(ivweb)最终选择了jsbundle 部分内置,业务资源动态下发的方式; 剩余的文章部分笔者将会一一介绍我们是如何优化 rn 的。

# 二、性能优化问题分析

回到上面讲的 jsbundle 部分内置,资源动态下发的方式,我们简单介绍一下其整体的思路:

  1. rn 框架启动
  2. 解析并执行公共 jsbundle, 并行下载业务 jsbundle;
  3. 解析并执行业务 jsbundle

在不考虑优化 rn 框架的基础上,我们得到下载业务 jsbundle 到页面渲染的大致耗时为:

很明显首屏耗时时间是非常长的;整体分析了 rn 的耗时时间,我们主要从如下几个方面考虑做性能优化:

  1. 优化 bundle 下载时间: 由于数据的隐私性,暂时只能提供整体优化后的首屏时间: 从 2046ms 到 1176ms
  2. 优化 bundle 解析时间
  3. 优化首屏数据

# 三、bundle 下载时间优化-之 bundle 体积优化

# 1. 问题分析

经过一系列的统计数据我们得到 bundle 下载的时间和资源大小的关系:

说明: bundle 的下载时间和资源的大小是成正比关系; 下面我们将具体探讨可以通过哪些方式优化 bundle 体积.

首先我们来分析一下一个 rn 的 bundle 由哪些部分组成: 图片资源 + bundle;

而一个 bundle 又由: 全局变量声明+polyfill+模块定义+require四部分组成


// var声明层

var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{};process.env=process.env||{};process.env.NODE_ENV=process.env.NODE_ENV||"production";

//polyfill层

!(function(r){"use strict";r.__r=o,r.__d=function(r,i,n){if(null!=e[i])return;var o={dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}};e[i]=o}


...
// 模块定义层
__d(function(g,r,i,a,m,e,d){var n=r(d[0]),t=r(d[1]),o=n(r(d[2])),u=r(d[3]);t.AppRegistry.registerComponent(u.name,function(){return o.default})},0,[1,2,402,403]);
....
__d(function(a,e,t,i,R,S,c){R.exports={name:"ReactNativeSSR",displayName:"ReactNativeSSR"}},403,[]);

// require层
__r(93);
__r(0);

所以我们优化的思路大致有如下几个方向:

  1. 减少图片资源,图片地址 cdn 化: 图片资源并非和 bundle 一起下发而是使用 cdn 的方式加载;
  2. 抽离公共包: 将业务通用的资源/代码进行抽离,比如 react,react-native,redux,以及一些业务通用的包内置到 app; 这样上面介绍的 bundle 就不会存在var 声明层,polyfill 层,部分 modules
  3. module tree shaking: 将无用的代码移除掉
  4. module diff: 将前后两次代码进行 diff 比较,只动态下发 diff 部分;

# 2. 减少图片资源,图片地址 cdn 化

减少图片资源,图片地址 cdn 化的本质是: 图片等资源无需内置到 app,采用线上加载的方式,从而减少整个下发包的体积

大家默认知道 rn 打包后的图片并不会进行 hash,所以减少图片资源,图片地址 cdn 化的前提是需要进行图片 hash.

这里推荐自己编写的一个 hash 库,结合 metro 进行使用: react-native-file-hash-plugin (opens new window);

文件进行 hash 之后需要将图片地址从本地地址更换为远程地址:

Image.resolveAssetSource.setCustomSourceTransformer((resolver) => {
  // 资源的appName,构建的时候插入
  const imageAppName = resolver.asset && resolver.asset.appName;
  // 拿到app 对应的cdn地址
  const bundleRoot = global.window[`${imageAppName}_cdnUrl`];
  let resolveAsset;
  if (bundleRoot) {
    resolver.jsbundleUrl = bundleRoot;
    if (Platform.OS === "android") {
      resolveAsset = resolver.drawableFolderInBundle();
    } else {
      resolveAsset = resolver.scaledAssetURLNearBundle();
    }
  } else {
    resolveAsset = resolver.defaultAsset();
  }
  return resolveAsset;
});

# 3. 抽离公共包

这块相信大家都做过,我们可以使用 metro 的processModuleFilter过滤掉哪些我们不需要打包到业务 bundle 的模块.

const commonModules = ["react", "react-native", "redux", "自己的业务模块"];
// 一个简单的例子
function processModuleFilter(module) {
  //过滤掉path为base-modules的一些模块(基础包内已有)
  if (module["path"].indexOf("base-modules") >= 0) {
    return false;
  }
  //过滤掉node_modules内的一些通用模块(基础包内已有)
  for (const ele of commonModules) {
    if (module["path"].indexOf("node_modules" + ele) > 0) {
      return false;
    }
  }
  //其他就是应用代码
  return true;
}

# 3. module tree shaking

这块大家可以参考一下我之前写的另外一篇文章:ReactNative 千人千面方案 (opens new window) 进行全局最小依赖分析,剔除无用模块;

# 4. module diff

module diff的主要思路为必须前后两次构建的资源包,进行 module diff;这样下发 jsbundle 的时候只下发增量部分。这里一般主要和离线包进行配合使用,要考虑的有:

  • 如何进行代码 diff?
  • 离线包的代码如何和增量部分进行组装?
  • 是否采用懒加载的方式加载 diff?

这块要做的事情很多,但是的确是有用处的。

# 四、bundle 下载时间优化-之 优化 http

众所周知 http 从 1.0 逐步发展到 http3.0;中间也涌现出 http2.0 这种使用支持多路复用的协议;但是 http2.0 并没有解决 TCP 的队头阻塞问题;这是由 TCP 协议所决定的。

所以为什么我们不能尝试使用 http3.0, QUIC 协议去做下载呢?

QUIC(Quick UDP Internet Connections,读 quick)是由 Google 提出的一种基于 UDP 改进的低时延的互联网传输层.

QUIC 相比 http2.0 有如下优点:

  • 支持 0-RTT 连接(最多 1-RTT)
  • 用户域的拥塞控制,协议可快速部署、更新
  • UDP 天然无队头阻塞问题
  • 连接迁移
  • 前向纠错

具体详细的使用可以查看: QUIC 协议在 Android 和 iOS 的使用 (opens new window)

# 五、优化 bundle 解析时间 - 并行加载

看过我之前写的React Native 与 iOS 通信原理三部曲

  1. ReactNative 与 iOS 原生通信原理解析-初始化原理 (opens new window)

  2. ReactNative 与 iOS 原生通信原理解析-JS 加载及执行篇 (opens new window)

  3. ReactNative 与 iOS 原生通信原理解析-通信篇 (opens new window)

的同学应该大致清楚 rn 解析的整个流程,刨除对 rn 底层的改写,影响 bundle 解析的时间有:

  • jsbundle 的体积: 这块我们已经在上面优化过 bundle 的体积了
  • common/业务包加载的耗时;

我们采用的方式是: 应用启动开启独立线程加载 common 包,页面启动后并行加载 bundle 资源的方式

# 六、优化 bundle 解析时间 - RN 单例/多例

# 单例

RN 的单例模式指的是: app 只会启动一个 rn 的实例,加载一次 common 包,多个业务包在同一个实例中运行

  • 单例模式的优点: 每个业务只会加载一次,若已经加载过则无需再次加载,内存消耗比较低;

  • 单例模式的缺点: 其中一个比较重要的问题是如何做数据的隔离? 多个业务包如果修改同一个全局变量,势必会造成数据混乱;所以我们需要做数据隔离;

我们这边做单例模式下的数据隔离使用的方式是: 使用 AST 改写一些全局变量

# 多例

RN 的多例模式指的是: 业务包每次都运行在一个单独的 RN 实例中,每次会单独的创建一个新的 RCTBridge

  • 多例模式的优点:天然的数据隔离

  • 多例模式的缺点: 多次页面的加载造成不必要的内存消耗;

# 性能优化-cgi 并行

这里的主要思路是: 在下载 rn bundle 的时候,并行请求首屏数据

详细的实现方式需要根据业务的具体场景。

# 其他性能优化/rn 相关技术

还有很多其他的性能优化手段,本文就不一一列举了,有兴趣的同学可以尝试一下:

# 最后

肯定会有同学有疑问,为啥我们不做rn的底层优化? 我们的主要考虑是:升级迭代的问题,通过我们的动态化方案,rn的升级变得没有那么困难,并且rn官方也一直致力于底层的优化。何不站在巨人的肩膀上呢?

【未经作者允许禁止转载】 Last Updated: 2/4/2024, 6:06:40 AM