RN 构建包体积过大,如何瘦身?

@[toc]

感觉很常见:业务说包太大,审核又催你上新,团队一头雾水。别急,包体积可以从 4 个方向系统化收敛:

  1. JS 逻辑(引入、依赖、打包方式)
  2. 图片 / 媒体资源(格式、密度、加载)
  3. 原生平台构建设置(Hermes、Proguard、AAB、App Thinning)
  4. 发布策略(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) 去除/替换臃肿第三方库

  • 替换 momentdayjsdate-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 切分

  1. 启用 minifyEnabled / shrinkResources(release)
gradle 复制代码
buildTypes {
    release {
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}
  • minifyEnabled 用于移除未引用的代码;shrinkResources 移除未引用的资源。配合正确的 proguard-rules.pro,能显著降低 APK 大小。
  1. 发布 AAB(App Bundle)
  • Google Play 的 AAB 会按设备拆分资源和 native 库,避免把所有 ABI 全打包到用户的安装包。发布 AAB 一般能减小下载包(用户只下对应 ABI)。
  • 本地测试可用 ./gradlew bundleRelease 生成 AAB。
  1. ABI 切分(如果不使用 AAB)
gradle 复制代码
splits {
    abi {
        enable true
        reset()
        include 'armeabi-v7a', 'arm64-v8a'
        universalApk false
    }
}
  • 为每个目标 abi 构建单独 APK,减少每个 APK 的大小(但需要多渠道管理)
  1. 移除不必要的 native libs
  • 检查 android/app/src/main/jniLibs 或第三方库是否带了大量 .so,删除未使用的架构或替换掉体积巨大的库。

iOS:App Thinning、Bitcode、资源裁剪

  1. App Thinning(On-Demand Resources & slicing)
  • iOS 会对设备做 slicing,App Store 会下发适配设备的 slice。尽量不要把所有资源放在 Assets.car 中为所有设备打包,而是使用 On-Demand Resources(按需资源)或在运行时下载图片。
  1. Bitcode
  • Bitcode 会影响 ipa 大小上传后处理,开启 / 关闭需基于你对后续 Re-Signing 的需求来权衡(通常不直接影响下载器端的体积,但会影响构建时的二进制分发流程)。
  1. 精简图片 / 仅保留必要分辨率
  • 同 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"
  }
}

执行顺序:

  1. npm run bundle:android
  2. npm run analyze:bundle → 打开 android-output/report.html 看占比
  3. 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,并实现网络降级策略。

每步都做「量化前后对比」------否则你不知道到底省了多少。

相关推荐
passerby606137 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc