@[toc]
感觉很常见:业务说包太大,审核又催你上新,团队一头雾水。别急,包体积可以从 4 个方向系统化收敛:
- JS 逻辑(引入、依赖、打包方式)
- 图片 / 媒体资源(格式、密度、加载)
- 原生平台构建设置(Hermes、Proguard、AAB、App Thinning)
- 发布策略(CodePush、增量包、分包)
下面把每一项拆开讲清楚,"为什么生大包、怎么量化、怎么改、改了会怎样"。每一章都有可运行的示例脚本或配置。
一、先量化:你包里都有什么?(必做)
不先量化就盲改。实际做法是:先生成生产 bundle + source map,然后把 bundle 解构(分析占比)------你才能知道"下一步先改哪一项"。
推荐流程(可复现):
bash
# 生成Android JS bundle(或 iOS 换 platform)
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output ./android-output/index.android.bundle \
--sourcemap-output ./android-output/index.android.bundle.map
# 安装并用 source-map-explorer / webpack-bundle-analyzer 来分析
npm install --save-dev source-map-explorer
npx source-map-explorer ./android-output/index.android.bundle ./android-output/index.android.bundle.map
解释:
- 生成 bundle 的命令会产出 JS 的真实大小(压缩前/压缩后)与 source map,
source-map-explorer可以生成可视化报告,告诉你哪些模块占比最高(比如某个 UI 库、moment、lodash 某个子包等)。
如果你用 Metro 的 ram-bundle 或 hermes,这套方法仍然适用(需要相应生成 bundle 的参数)。
二、JS 层的常见瘦身策略(和示例)
主要思路:减少首次要下载/打包的 JS 量、避免把大库一次性打进主 bundle、用更轻量实现替换大依赖。
1) 启用 Hermes(对 Android / iOS 都能显著影响)
Hermes 能减小 APK 的 JS 引擎体积(相比内嵌 V8/JSCore),并能减少 JS bundle 的初始内存与启动时消耗(在很多项目里能减小 APK 几 MB 到几十 MB,取决于平台与配置)。
Android 开启示例(android/app/build.gradle)
gradle
project.ext.react = [
enableHermes: true, // true = Hermes, false = JSC
]
apply from: "../../node_modules/react-native/react.gradle"
然后在根目录运行 cd android && ./gradlew assembleRelease 重新构建。
iOS(Podfile) (新版本 RN 支持 iOS Hermes)
在 Podfile 中启用 Hermes(不同 RN 版本稍有差异,按 RN 官方说明配置),然后 pod install。(注:iOS 启 Hermes 会改变二进制体积走向,通常能减少 JSCore 的 footprint,但你需要自己测量最终 ipa 的大小差异)
注意:开启 Hermes 后,某些第三方库需要 Hermes 兼容的预编译或额外配置(如 native模块、源码依赖)。
2) inlineRequires / RAM bundle(延后加载不常用模块)
在 metro.config.js 中启用 inlineRequires: true,能把模块的 require 延后到第一次使用时再执行,从而减轻首屏包体积与启动时解析开销。
metro.config.js
js
// metro.config.js
const { getDefaultConfig } = require('metro-config');
module.exports = (async () => {
const defaultConfig = await getDefaultConfig();
return {
transformer: {
...defaultConfig.transformer,
// 启用 inlineRequires,能显著减少主包首屏解析时间与首包大小(按场景)
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};
})();
效果:首次 bundle size 不变(字节级),但启动时解析/执行成本降低,用户感知上"变小了"。
另外,Metro 支持 RAM bundles(indexed/filestore),RAM bundle 更适合非常大的应用(并发加载模块),但兼容性和构建复杂度更高。可视项目需求评估是否启用。
3) 动态 import / 代码分割(按路由拆包)
把非首屏的大模块拆到异步 chunk,通过 import() 动态加载。例如复杂的编辑页面、设置页、视频列表等。React Native 的动态 import 在 Metro 与 Hermes 下要注意兼容,但通常可行。
示例:
js
// 在路由里按需 load
const LazyEditor = React.lazy(() => import('./screens/EditorScreen'));
function App() {
return (
<Suspense fallback={<View><Text>loading...</Text></View>}>
<LazyEditor />
</Suspense>
);
}
注意:React.lazy 在 RN 上的兼容性较好,但需配合 Metro 的配置与 Babel 插件(确保 transform-runtime 支持)。
4) 去除/替换臃肿第三方库
- 替换
moment→dayjs或date-fns(体积差别明显)。 - lodash:使用
lodash-es或只按需引入lodash/get等单个包。 - 大 UI 框架:评估是否真的需要整套 UI,还是把常用组件抽成小库或使用按需加载。
示例:按需替换 lodash
js
// 不要
import _ from 'lodash';
// 推荐
import get from 'lodash/get';
5) Tree-shaking 与 babel 优化
确保 Babel 配置支持按需打包(避免把整个库拉进来)。对第三方库如果暴露的是 CommonJS,有时会无法 tree-shake,这时考虑用更轻量替代。
三、图片 / 媒体资源优化(最能直接砍体积的一块)
常见问题:把所有图片打到 app bundle(尤其 Android 的 drawable-xxx),导致安装包暴涨。解决思路:压缩、转换更高压缩格式、按需下载而不是打包、使用 CDN、SVG / icon font 替代图片。
1) 把静态资源转成 WebP / Avif(平台支持情况下)
WebP 能带来可观压缩比。iOS 支持 WebP via libraries(或转换时生成 PNG 兼容版本)。Android 原生支持 WebP。处理脚本示例(使用 sharp):
image-optimize.js(Node + sharp)
js
// image-optimize.js
// 安装:npm i sharp glob
const sharp = require('sharp');
const glob = require('glob');
const path = require('path');
const fs = require('fs');
const SRC = './assets/images';
const OUT = './assets/images_opt';
if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true });
glob.sync(`${SRC}/**/*.{png,jpg,jpeg}`).forEach(file => {
const rel = path.relative(SRC, file);
const out = path.join(OUT, rel.replace(/\.(png|jpg|jpeg)$/, '.webp'));
sharp(file)
.resize({ width: 1200, withoutEnlargement: true })
.webp({ quality: 75 })
.toFile(out)
.then(() => console.log('optimized', out))
.catch(err => console.error(err));
});
- 把高分辨率图像转 WebP 并降低质量到 70~80,通常能把体积降低 40%+。
- 然后在打包时只把
images_opt当做资源(或把 WebP 上传 CDN)
2) 对于多分辨率资源(Android / iOS)
不要把所有分辨率都打包进 APK:只保留必要密度或使用矢量(SVG)/字体图标(icon font)。另外,Android App Bundle(AAB)会做 density splitting(如果你发布 AAB,Google Play 会为不同设备下发不同的资源)。如果你仍使用 APK,要在构建时考虑 resConfigs 只包含目标语言/密度。
Android build.gradle snippet
gradle
android {
defaultConfig {
// 限制语言/屏幕密度,减小资源体积(按需)
resConfigs "en", "xxhdpi"
}
}
3) 把大图放到 CDN,运行时按需下载
把少用或大体积图片(如专题图片、背景图)放到服务器或 CDN,在 App 首次运行时从网络获取,或缓存到本地。静态小图保存在 bundle。结合 react-native-fast-image 做缓存。
4) 动态压缩/裁剪(服务端)
服务端按分辨率返回合适尺寸的图(large/medium/small),移动端只请求合适的尺寸,避免下载过大图。
四、原生平台瘦身(Android / iOS 差异化策略)
Android:Proguard / R8、App Bundle、ABI 切分
- 启用 minifyEnabled / shrinkResources(release)
gradle
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
minifyEnabled用于移除未引用的代码;shrinkResources移除未引用的资源。配合正确的proguard-rules.pro,能显著降低 APK 大小。
- 发布 AAB(App Bundle)
- Google Play 的 AAB 会按设备拆分资源和 native 库,避免把所有 ABI 全打包到用户的安装包。发布 AAB 一般能减小下载包(用户只下对应 ABI)。
- 本地测试可用
./gradlew bundleRelease生成 AAB。
- ABI 切分(如果不使用 AAB)
gradle
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a'
universalApk false
}
}
- 为每个目标 abi 构建单独 APK,减少每个 APK 的大小(但需要多渠道管理)
- 移除不必要的 native libs
- 检查
android/app/src/main/jniLibs或第三方库是否带了大量.so,删除未使用的架构或替换掉体积巨大的库。
iOS:App Thinning、Bitcode、资源裁剪
- App Thinning(On-Demand Resources & slicing)
- iOS 会对设备做 slicing,App Store 会下发适配设备的 slice。尽量不要把所有资源放在
Assets.car中为所有设备打包,而是使用 On-Demand Resources(按需资源)或在运行时下载图片。
- Bitcode
- Bitcode 会影响 ipa 大小上传后处理,开启 / 关闭需基于你对后续 Re-Signing 的需求来权衡(通常不直接影响下载器端的体积,但会影响构建时的二进制分发流程)。
- 精简图片 / 仅保留必要分辨率
- 同 Android 的策略:SVG、PDF vector assets(Xcode 能对 PDF 做多分辨率导出)。
五、CodePush 的体积影响(注意误区)
很多团队听说用 CodePush 就能"瞬间瘦身",其实要理性看:
-
CodePush 只能更新 JS bundle 和资源(热更新),它不会改变原生二进制(APK/IPA)。因此:
- 如果你的包大是因为原生库(
.so, frameworks, or native bundles),CodePush 解决不了这类体积。 - 如果你的包大是因为 JS 代码或大量静态资源嵌入在 bundle(如 images/),CodePush 可以把这些资源拆出去并做按需下载,从而减少首次安装包的大小(通过把资源放到 CodePush 而不是原生 bundle)。但这会变成运行时首次下载的问题(用户下载安装后可能会在第一次启动时网络下载),需评估网络体验与合规性(有些商店/审核对 CodePush 类型热更有要求)。
- 如果你的包大是因为原生库(
实战建议:
- 把真正可以网络下发的资源放入 CodePush(如非关键 UI 图、可替换的内容),但不要把必须运行的 native 代码放入 CodePush(这不可能)。
- 在 App 首次启动设计好回退策略(网络不通时回退到本地资源)。
六、避免"只看构建体积而忽略启动体验"的陷阱
有时候我们把注意力放在 APK/IPA 的"安装包"大小上,但用户更关心"下载/更新/启动体验"。举例:
- 把 JS bundle 移到 CodePush 可以减小安装包,但会增加首次运行的网络下载与启动延迟。
- 启 Hermes 或拆 ABI 会把安装包变小,但如果同时增加了首次运行时的解压或初始化任务,用户感知可能变差。
因此:同时衡量:安装包大小、首次启动时间、首次渲染时间、冷启动内存峰值。
七、来源清单:一套你可以直接跑的"瘦身工具箱"脚本(实战 demo)
下面给出一套脚本/配置片段,你能直接拷贝进项目,按顺序执行并量化每一步效果。
1) package.json scripts(分析 + 生成 bundle)
json
{
"scripts": {
"bundle:android": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output ./android-output/index.android.bundle --sourcemap-output ./android-output/index.android.bundle.map",
"analyze:bundle": "npx source-map-explorer ./android-output/index.android.bundle ./android-output/index.android.bundle.map --html ./android-output/report.html",
"optimize:images": "node scripts/image-optimize.js"
},
"devDependencies": {
"source-map-explorer": "^2.5.2",
"sharp": "^0.32.1",
"glob": "^7.2.0"
}
}
执行顺序:
npm run bundle:androidnpm run analyze:bundle→ 打开android-output/report.html看占比npm run optimize:images→ 把图片压成 WebP 或更合适尺寸
2) metro.config.js(inlineRequires)
(前文已给,拷贝复用)
3) android/app/build.gradle(Hermes + Proguard)
(前文已给片段,补充完整 release 配置)
gradle
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
project.ext.react = [
enableHermes: true
]
4) 简单的图片优化脚本(前文已给 image-optimize.js)
八、降重后的验证(你需要看哪些指标)
每改一次,务必对比下面指标(量化才能说动作有效):
- APK / AAB / IPA 的最终字节数(安装包大小)
- JS bundle gzipped 大小(
gzip压缩后的大小更接近网络传输量) - 首次冷启动时间(cold start)与首屏渲染时间(Time to render)
- 内存峰值(特别是启动时)
- Crash / ANR 数量(某些瘦身步骤如启 Hermes/Proguard 会引入 runtime 错误)
把这些数据放到 CI pipeline,每次 PR 都跑一次自动分析(至少 bundle + source-map-explorer 报告),能让你在代码变动中持续监控体积变化。
九、实战案例(TIPS & 血的教训)
- 把图片全放 bundle 是最常见误区:把 100+ 张高分辨率图片放在 assets 下,会让安装包暴增几十 MB。解决:CDN / On-demand / WebP。
- 一次性把 lodash / moment / full UI 库引进来会拉高 bundle 比例。解决:按需引入 + 替换轻量库。
- 开启 Hermes 后部分第三方 SDK 会 crash(要做回归),所以先在内部 Beta 机上跑一批真机测试再放量。
- 发布 AAB 后,测试自己的 APK 与 AAB 的差异(不同设备拿到的切片可能变体)。
十、总结(一个可立即执行的 7 天计划)
给你一个落地的 7 天行动计划(按优先级):
Day 1:生成并分析当前 bundle(source-map-explorer),列出 top-10 大体积项。
Day 2:把图片批量压缩/转换 WebP 并替换或上 CDN,重新对比大小。
Day 3:启用 metro.inlineRequires;在本地和真机测启动与首屏表现。
Day 4:评估大库替换(moment → dayjs、lodash 精简),做小 PR。
Day 5:Android 打开 minifyEnabled+shrinkResources,生成 AAB 并验证安装(保留备份签名)。
Day 6:考虑启用 Hermes(先 Beta),回归所有关键页面。
Day 7:把不能在安装包里放的资源放到 CodePush 或 CDN,并实现网络降级策略。
每步都做「量化前后对比」------否则你不知道到底省了多少。