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,并实现网络降级策略。

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

相关推荐
小光学长2 小时前
基于web的影视网站设计与实现14yj533o(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·前端·数据库
vocoWone2 小时前
📰 前端资讯 - 2025年12月10日
前端
李少兄2 小时前
前端开发中的多列布局(Multi-column Layout)
前端·css
new出一个对象2 小时前
uniapp手写滚动选择器
开发语言·前端·javascript
Data_agent2 小时前
京东获得京东商品详情API,python请求示例
java·前端·爬虫·python
CodeSheep2 小时前
这个知名编程软件,正式宣布停运了!
前端·后端·程序员
2401_860319522 小时前
DevUI组件库实战:从入门到企业级应用的深度探索,如何实现带搜索的Table表格
前端·前端框架
m0_471199633 小时前
【场景】用户名+密码+验证码的登录全流程
前端
恋猫de小郭3 小时前
豆包手机为什么会被其他厂商抵制?它的工作原理是什么?
android·前端·ai编程