React Native 打包体积优化指南:在保证功能正常的前提下把包压到最小
适用版本:React Native 0.70+(Hermes 已成为默认引擎),Android & iOS 双端。
核心思路:先量化,再削减,最后验证。没有测量的优化是盲目的,没有验证的削减是危险的。
目录
- 第一步:先搞清楚包里面有什么
- [JavaScript 侧优化](#JavaScript 侧优化)
- [Android 原生侧优化](#Android 原生侧优化)
- [iOS 原生侧优化](#iOS 原生侧优化)
- 资源文件优化(双端通用)
- 依赖治理:最容易被忽视的大头
- 发版形态:让商店帮你瘦身
- 如何保证功能正常:验证清单
- 一张速查表
一、先搞清楚包里面有什么
任何优化之前,先分析包体构成。
Android
- 用 Android Studio 的 APK Analyzer (Build → Analyze APK)直接打开 APK/AAB,可以看到
classes.dex、lib/(各架构的 .so)、res/、assets/各占多少。 - 重点关注:
lib/arm64-v8a、lib/armeabi-v7a等目录:原生库(包括 React Native 自己的libreactnativejni.so、Hermes 引擎、第三方原生模块)。assets/index.android.bundle:JS 产物(开启 Hermes 后是字节码)。res/和assets/里的图片、字体。
iOS
- 在 Xcode 中 Product → Archive 后,用 App Store Connect 的 App Thinning Size Report 查看各设备的实际下载体积。
- 本地可以解压
.ipa(改后缀为 zip),查看Payload/App.app里的二进制主文件、Assets.car、JS bundle、字体等。
JS Bundle
用工具分析 JS bundle 里哪些模块占体积:
bash
# 方案一:source-map-explorer
npm install -g source-map-explorer
# 先生成带 sourcemap 的 bundle,再分析
source-map-explorer index.android.bundle index.android.bundle.map
# 方案二:react-native-bundle-visualizer(更方便)
npx react-native-bundle-visualizer
经验值:一个空的新建 RN 项目,Android APK(单架构 + Hermes)约 8--12 MB;如果业务包到了 50 MB+,通常图片资源和第三方依赖是重灾区。
二、JavaScript 侧优化
2.1 使用 Hermes 引擎(新版默认开启)
Hermes 把 JS 预编译为字节码,包内不再需要携带 JS 引擎解释执行源码,bundle 更小、启动更快、内存更低。
js
// android/app/build.gradle(RN 0.70+ 默认 true)
project.ext.react = [
enableHermes: true,
]
ruby
# ios/Podfile
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true
)
注意:切换 Hermes 后一定要全量回归测试,个别依赖 JIT 的库(如老版本
realm、某些 polyfill)可能不兼容。
2.2 Metro 配置:压缩与内联优化
js
// metro.config.js
module.exports = {
transformer: {
// 开启内联 require:延迟加载模块,减小启动开销,bundle 更紧凑
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
minifierPath: 'metro-minify-terser',
minifierConfig: {
compress: {
drop_console: true, // 生产环境移除 console.*
drop_debugger: true,
pure_funcs: ['console.log', 'console.info', 'console.debug'],
},
mangle: true, // 变量名混淆缩短
output: {
comments: false, // 去掉注释
},
},
},
};
也可以用 babel 插件兜底去 console:
bash
npm i -D babel-plugin-transform-remove-console
js
// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
};
2.3 代码分割与按需加载
- 用
React.lazy+Suspense或路由库的懒加载能力,把非首屏页面拆出去。 - 大图表、富文本编辑器等"重功能"页面,做到进入时才加载。
- 避免在入口文件
import整个大工具库:
js
// ❌ 会把整个 lodash 打进 bundle
import _ from 'lodash';
// ✅ 只打包用到的方法
import debounce from 'lodash/debounce';
// ✅ 或者用 babel-plugin-lodash / babel-plugin-import 自动按需转换
2.4 别打不该打的
- 确保打包脚本用的是 release 模式,debug bundle 会大很多且未压缩。
.env开发配置、mock 数据、Storybook、测试代码不要进 release bundle。
三、Android 原生侧优化
3.1 按 ABI 拆分:立竿见影的大招
不同 CPU 架构的设备只需要对应的 .so 原生库。打 universal APK 会把 4 套架构全塞进去。
groovy
// android/app/build.gradle
android {
splits {
abi {
reset()
enable true
universalApk false // 不打全架构包
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
}
2026 年的今天,绝大多数国内分发只保留
arm64-v8a+armeabi-v7a即可,x86 系列基本只用于模拟器,可以再省一份。上传 Google Play 则直接用 AAB(见第七节),商店自动按设备分发。
3.2 开启代码混淆与资源压缩
groovy
android {
buildTypes {
release {
minifyEnabled true // R8 代码压缩、混淆、优化
shrinkResources true // 移除未引用的资源(必须配合 minifyEnabled)
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
zipAlignEnabled true
}
}
}
⚠️ 关键风险点 :shrinkResources 和 R8 会移除"看起来没被引用"的资源和类。RN 的反射调用、第三方 SDK 的 keep 规则没配好,就会运行时崩溃。务必:
- 保留 RN 必需规则(RN 官方 aar 已自带大部分 proguard 规则);
- 为三方 SDK 逐个添加官方文档要求的
-keep规则; - 用
tools:keep标记被动态引用的资源(res/raw/keep.xml)。
xml
<!-- res/raw/keep.xml:防止被反射引用的资源被 shrinkResources 删掉 -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/splash_*,@raw/notification_sound" />
3.3 压缩 .so 并启用 extractNativeLibs 控制
groovy
android {
packagingOptions {
jniLibs {
useLegacyPackaging false // .so 压缩存储,安装时解压
}
}
}
useLegacyPackaging = false:APK 内 .so 被压缩,下载体积小,但安装后占用略大(Android 6.0+ 推荐)。useLegacyPackaging = true:.so 不压缩并对齐,系统可直接从 APK 映射加载,安装后占用小但 APK 下载体积大。- 上架商店(走 AAB)时让商店处理即可,一般选
false。
3.4 精简语言资源
groovy
android {
defaultConfig {
resConfigs "zh-rCN", "zh-rTW", "en" // 只保留需要的语言
}
}
这一行可以砍掉所有第三方库自带的多语言字符串,通常能省几百 KB 到几 MB。
3.5 其他 Android 小技巧
- 移除未用模块 :检查
android/app/build.gradle的dependencies和settings.gradle,把已经删掉的 JS 库对应的原生模块一并移除。 - WebP 替代 PNG :
res/下的位图转 WebP(见第五节)。 - VectorDrawable 替代多分辨率 PNG:单色图标尽量用矢量图,一份文件适配所有密度。
- NDK 过滤 :如果项目里有自编译的 C/C++ 库,
ndk { abiFilters }只保留目标架构。
四、iOS 原生侧优化
4.1 开启编译优化
在 Xcode Target → Build Settings 中:
| 配置项 | 推荐值 | 作用 |
|---|---|---|
Optimization Level (Release) |
-Os(Optimize for Size) |
以体积优先优化 |
Strip Linked Product |
Yes |
去除符号表 |
Strip Style (Release) |
All Symbols 或 Non-Global Symbols |
进一步精简符号 |
Dead Code Stripping |
Yes(默认) |
移除无用代码段 |
Symbols Hidden by Default |
Yes |
减少导出符号 |
Generate Debug Symbols (Release) |
No(符号表交给 dSYM 文件单独保存) |
二进制不带调试符号 |
注意:
Yes关掉调试符号后,务必保存每次发版的 dSYM 文件,否则线上 crash 堆栈无法符号化。接入 Sentry/Bugly 等平台时上传 dSYM。
4.2 App Thinning(交给 App Store 自动做)
上传 App Store 的构建会自动享受:
- Slicing:按设备只下发对应架构(arm64)和对应分辨率(@2x/@3x)的资源;
- On-Demand Resources (ODR):非首屏必需的大资源(教学视频、游戏关卡素材)标记为按需下载;
- 静态库的 Bitcode 重新编译(注:Xcode 14+ 已废弃 Bitcode,不用管)。
Asset Catalog 是前提 :图片必须放进 Assets.xcassets 才能被 slicing,散放在 bundle 里的图片无法按分辨率裁剪。
4.3 只保留 arm64 架构
RN 0.70+ 的项目,Release 构建默认只打 arm64(iPhone 5s 之后全支持)。检查 Build Settings → Architectures,不要遗留 armv7。
4.4 精简 Pod 依赖
pod install后检查Podfile.lock,删掉 JS 侧已经卸载的库(很多人删了 npm 包忘了pod install清理)。- 对特别大的三方 SDK(推送、统计、IM),评估是否所有功能都用得上,很多 SDK 支持子模块按需引入(
pod 'XXX/Core'而不是整个pod 'XXX')。
五、资源文件优化(双端通用)
资源往往是体积优化的性价比之王,一个没压缩的启动图可能比整个 JS bundle 还大。
5.1 图片
bash
# 批量把 PNG/JPG 转 WebP(平均再省 25%--70%)
cwebp -q 80 input.png -o output.webp
# 无损压 PNG
pngquant --quality=65-80 --ext .png --force *.png
- RN 原生支持 WebP(Android 原生支持;iOS 需要
react-native-webp-format或 SDWebImage 支持,新版本 RN 的 iOS 已内置支持)。 - 大图(背景、Banner)优先 WebP;纯色图标用 SVG(
react-native-svg)或 iconfont 字体。 - 图标用 iconfont:几百个图标只占几十 KB。
5.2 字体
- 中文字体动辄 10 MB+。如果只为了数字/少量特殊字符,用 字体子集化(subset)只保留用到的字符:
bash
pip install fonttools
pyftsubset PingFang.ttf --text-file=used-chars.txt --output-file=PingFang-subset.ttf
几百 KB 即可覆盖常用数字和标点。
5.3 音视频、Lottie
- 内置视频改为在线加载 + 本地缓存。
- Lottie JSON 可以用 lottie 压缩工具 或手动删掉冗余字段;超大动画考虑换成序列帧 WebP/视频。
六、依赖治理:最容易被忽视的大头
定期执行:
bash
# 找出占用最大的依赖
npx dependency-cruiser --version # 或用下面这个更直观
npm ls --all | sort
# 检查重复和过期依赖
npx depcheck # 找未使用的依赖
npm dedupe # 消除重复版本
常见"重量级"依赖的平替
| 重 | 轻 | 说明 |
|---|---|---|
moment(~300KB,还带一堆 locale) |
dayjs(~2KB) / date-fns(按需) |
API 几乎一致 |
lodash 全量 |
lodash/debounce 按需引入 或 lodash-es + tree shaking |
见 2.3 |
| 整个 UI 组件库 | 按需引入 + babel-plugin-import |
检查是否有未用主题被打入 |
| 多套图表库 | 只留一套 | 很多项目历史遗留 echarts + victory + chart.js 并存 |
重复的 react-native-vector-icons 字体 |
只 include 用到的字体族 |
默认会把十几套 icon 字体全打进去! |
vector-icons 专项(Android):
groovy
// android/app/build.gradle 顶部
project.ext.vectoricons = [
iconFontNames: ['MaterialIcons.ttf', 'FontAwesome.ttf'] // 只留用到的
]
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
iOS 则删掉 Info.plist 里 UIAppFonts 中不用的字体声明,并从 Xcode 资源里移除对应文件。
原生依赖同理
- 第三方 SDK(推送 × 2 家、统计 × 3 家这种历史遗留)是重灾区,定期梳理下线。
- 每次升级 RN 版本本身也常常带来引擎层面的瘦身。
七、发版形态:让商店帮你瘦身
Android:AAB(Android App Bundle)
bash
cd android
./gradlew bundleRelease # 产出 app-release.aab
上传到 Google Play 后,Play 会按设备动态生成只含所需架构、语言、分辨率的 APK。用户实际下载体积通常比 universal APK 小 30%--50%。国内渠道(华为、小米、OPPO、vivo、应用宝)也均已支持 AAB 或各家自己的动态分发。
注意:提交 AAB 需要在 Play 后台开启 Play App Signing,妥善保管签名密钥。
iOS:天然享受 App Thinning
只要资源进了 Asset Catalog、按规范出包,App Store 的 slicing 自动生效,无需额外操作。重点看 App Store Connect 里的体积报告验收效果。
八、如何保证功能正常:验证清单
体积优化最大的风险是"压过头把功能压坏了"。以下清单按顺序执行:
8.1 构建期验证
- Release 构建通过,无 ProGuard/R8 警告(有 warning 不要忽略,逐条确认或加 keep 规则)。
- bundle 大小对比记录在案(每次发版记录,防止某次依赖升级悄悄膨胀)。
8.2 自动化测试
- 单元测试全绿(尤其工具类被混淆后,JSON 序列化/反射相关代码)。
- 核心链路 UI 自动化(Detox / Appium):登录、支付、核心业务流,必须跑 Release 包而不是 Debug 包------很多问题只在混淆/压缩后出现。
8.3 针对每项优化点的专项回归
| 优化手段 | 重点回归项 |
|---|---|
| Hermes | 全量回归,重点测用到 Proxy、正则、JSON 大数的功能和第三方 SDK |
drop_console |
确认没有逻辑依赖 console 副作用(少见但存在) |
| R8 / 混淆 | 反射调用的类、Gson/Fastjson 序列化的 Model、JNI 回调、推送/支付 SDK 回调 |
shrinkResources |
通过 getIdentifier() 动态加载的资源(最常见踩坑点!) |
| ABI 拆分 | 每个渠道包的安装测试,确认没把 armeabi-v7a 设备漏掉 |
| WebP 替换 | Android 5.0 以下老设备、iOS 低端机的图片显示 |
| 字体子集化 | 生僻字、特殊符号是否显示为豆腐块 □ |
| 删语言资源 | 系统切换英文/繁体时应用是否正常 |
| 依赖替换 | moment → dayjs 的时区、本地化格式差异 |
8.4 灰度与监控兜底
- 灰度发布:先 1%--5% 用户,观察 24--48 小时。
- 崩溃监控:Sentry / Bugly / Firebase Crashlytics,务必上传对应版本的 sourcemap(JS)和 dSYM / mapping.txt(原生混淆还原表),否则报错看不懂。
- 关键指标对比:崩溃率、ANR 率、启动耗时在新包上不应劣化------Hermes + 按需加载通常还会让启动变快。
8.5 一条铁律
每次只改一类优化,验证通过后再叠加下一项。 一口气上十个优化,出了问题根本定位不到是哪个引起的。
九、一张速查表
| 层面 | 手段 | 预估收益 | 风险 |
|---|---|---|---|
| 发版形态 | AAB / App Store Slicing | ⭐⭐⭐⭐⭐ | 低 |
| Android | ABI 拆分(只留 arm64 + v7a) | ⭐⭐⭐⭐⭐ | 低 |
| Android | minify + shrinkResources + R8 | ⭐⭐⭐⭐ | 中(反射/动态资源) |
| Android | resConfigs 精简语言 | ⭐⭐ | 低 |
| JS | Hermes 引擎 | ⭐⭐⭐ | 中(兼容性回归) |
| JS | 去 console + terser 压缩 | ⭐⭐ | 低 |
| JS | 代码分割 / 懒加载 | ⭐⭐⭐(首屏) | 低 |
| 依赖 | moment→dayjs、lodash 按需、去重复依赖 | ⭐⭐⭐ | 中(API 差异) |
| 依赖 | vector-icons 只保留用到的字体 | ⭐⭐⭐ | 低 |
| 资源 | PNG→WebP、SVG/iconfont、字体子集 | ⭐⭐⭐⭐ | 中(老设备兼容) |
| iOS | -Os 优化 + Strip 符号 | ⭐⭐ | 低(保存 dSYM) |
| iOS | Asset Catalog + ODR | ⭐⭐⭐ | 低 |
| 双端 | 卸载残留的原生模块/三方 SDK | 视情况,可能很大 | 中 |
结语
包体积优化没有银弹,它是一个"测量 → 削减 → 验证 → 再测量"的循环。建议:
- 把 APK/IPA 体积 和 bundle 体积 纳入 CI,每次构建自动记录,超标报警;
- 每次引入新依赖前,先用
react-native-bundle-visualizer看一眼它多大; - 每半年做一次依赖大扫除。
做到这三点,你的包体积就永远不会失控。