# 在腾讯我们是怎么做rn的性能优化的
这篇文章的标题可能有点大,作者的本意是想给大家分享一下在腾讯的这段时间从事 react-native 相关的工作,是如何对 react-native(下文称 rn)进行性能优化的,也许这是你看到的最全的一篇 rn 性能优化相关的文章.
由于笔者后续会较少的参与 rn 相关的工作,这篇文章也算是自己对这么久以来 rn 工作的总结吧,也希望对部分正在路上的同学有所启发和帮助.
# 一、前言
rn 官方的默认推荐方式是大家使用 rn 开发并作为独立的 app 使用,但是对于较大体量的 app 考虑到框架迁移的成本以及对技术保持怀疑的态度 都不会直接使用 rn 开发一个独立的 app;反而更多的是将 rn 作为内部的一些独立模块/页面.
熟悉 rn 的同学应该都清楚,想要 rn 页面在 app 中启动需要内置其一堆底层框架,或者 rn 的 jsbundle; 而内置 rn 的 jsbundle 又分为两种,其优缺点也都较为明显;
jsbundle 全量内置
- 优点: 启动速度快
- 缺点: 每次更新 rn 页面都需要跟随 app 发版本,特别是针对页面出现重大 bug 的情况无法做到及时的更新;
jsbundle 部分内置
- 优点: 内置通用的 jsbundle,动态下发业务 bundle;可以动态更新页面
- 缺点: 加载速度相比内置会多一个下载时间,启动速度会有少许的缓慢;
看了上面两种 jsbundle 我们团队(ivweb)最终选择了jsbundle 部分内置,业务资源动态下发的方式; 剩余的文章部分笔者将会一一介绍我们是如何优化 rn 的。
# 二、性能优化问题分析
回到上面讲的 jsbundle 部分内置,资源动态下发的方式,我们简单介绍一下其整体的思路:
- rn 框架启动
- 解析并执行公共 jsbundle, 并行下载业务 jsbundle;
- 解析并执行业务 jsbundle
在不考虑优化 rn 框架的基础上,我们得到下载业务 jsbundle 到页面渲染的大致耗时为:
很明显首屏耗时时间是非常长的;整体分析了 rn 的耗时时间,我们主要从如下几个方面考虑做性能优化:
- 优化 bundle 下载时间: 由于数据的隐私性,暂时只能提供整体优化后的首屏时间: 从 2046ms 到 1176ms
- 优化 bundle 解析时间
- 优化首屏数据
# 三、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);
所以我们优化的思路大致有如下几个方向:
- 减少图片资源,图片地址 cdn 化: 图片资源并非和 bundle 一起下发而是使用 cdn 的方式加载;
- 抽离公共包: 将业务通用的资源/代码进行抽离,比如 react,react-native,redux,以及一些业务通用的包内置到 app; 这样上面介绍的 bundle 就不会存在var 声明层,polyfill 层,部分 modules
- module tree shaking: 将无用的代码移除掉
- 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 通信原理三部曲
的同学应该大致清楚 rn 解析的整个流程,刨除对 rn 底层的改写,影响 bundle 解析的时间有:
- jsbundle 的体积: 这块我们已经在上面优化过 bundle 的体积了
- common/业务包加载的耗时;
我们采用的方式是: 应用启动开启独立线程加载 common 包,页面启动后并行加载 bundle 资源的方式
# 六、优化 bundle 解析时间 - RN 单例/多例
# 单例
RN 的单例模式指的是: app 只会启动一个 rn 的实例,加载一次 common 包,多个业务包在同一个实例中运行
单例模式的优点: 每个业务只会加载一次,若已经加载过则无需再次加载,内存消耗比较低;
单例模式的缺点: 其中一个比较重要的问题是如何做数据的隔离? 多个业务包如果修改同一个全局变量,势必会造成数据混乱;所以我们需要做数据隔离;
我们这边做单例模式下的数据隔离使用的方式是: 使用 AST 改写一些全局变量
# 多例
RN 的多例模式指的是: 业务包每次都运行在一个单独的 RN 实例中,每次会单独的创建一个新的 RCTBridge
多例模式的优点:天然的数据隔离
多例模式的缺点: 多次页面的加载造成不必要的内存消耗;
# 性能优化-cgi 并行
这里的主要思路是: 在下载 rn bundle 的时候,并行请求首屏数据
详细的实现方式需要根据业务的具体场景。
# 其他性能优化/rn 相关技术
还有很多其他的性能优化手段,本文就不一一列举了,有兴趣的同学可以尝试一下:
- 添加离线包
- 资源预加载
- rn 页面同构 h5,做降级处理 (opens new window)
- ReactNative 做 abtest (opens new window)
- ReactNative 做 数据直出 (opens new window)
# 最后
肯定会有同学有疑问,为啥我们不做rn的底层优化? 我们的主要考虑是:升级迭代的问题,通过我们的动态化方案,rn的升级变得没有那么困难,并且rn官方也一直致力于底层的优化。何不站在巨人的肩膀上呢?