React + Vite 打包体积优化实战:把最大的 JS Chunk 从 4MB 压到 500KB

62 阅读3分钟

项目用 Vite + React,整体打包体积其实还能接受,但有一次做性能排查时发现了一个非常刺眼的问题:

build 后最大的单个 JS 文件超过了 4MB

这个 chunk 几乎参与了所有页面的加载,哪怕走了 gzip,对首屏加载依然非常不友好。

所以这次优化的目标也很明确:

不追求整体包体积数字好看,只针对这个“最大 JS Chunk”做瘦身。

最终结果是:

最大 JS Chunk 从 4000+ KB 压缩到了 500+ KB(未压缩体积)

下面记录一下这次优化的全过程。


一、先定位问题:为什么会有 4MB 的 JS 文件?

第一步没有急着改配置,而是先做了打包分析。

我在项目里加了 rollup-plugin-visualizer,重点看两个点:

  • 哪个 chunk 最大?
  • 这个 chunk 里都塞了什么?

结果非常典型:

  • 多个大型第三方库被打进了同一个 chunk
  • vendor 和业务代码严重耦合
  • Tree Shaking 虽然开了,但无能为力

👉 问题不是依赖多,而是它们被错误地“绑”在了一起。


二、核心思路:把“不该在一起的代码拆开”

这次优化的核心目标只有一个:

让这个 4MB 的 chunk 彻底“解体”

也就是说:

  • 大而稳定的依赖 → 单独拆
  • 非首屏必需的库 → 延迟加载
  • 能 tree shake 的 → 确保真的被 shake 掉

三、manualChunks:直接命中问题核心

我在 vite.config.ts 里针对第三方依赖做了手动拆包:

function manualChunks(id: string) {
  if (id.includes("node-forge")) return "node-forge";
  if (id.includes("lodash")) return "lodash";
  if (id.includes("decimal.js")) return "decimal";
  if (id.includes("lexical")) return "lexical";
  if (id.includes("refractor")) return "refractor";
  if (id.includes("ethers")) return "ethers";
  if (id.includes("i18next")) return "i18next";
  if (id.includes("axios")) return "axios";
}

为什么这一刀这么有效?

在优化前:

  • 这些库全部被打进同一个巨大 chunk
  • 任何一个页面加载,都会被迫下载它们

拆完之后:

  • 最大 chunk 体积立刻下降
  • 各个依赖按需加载
  • 浏览器缓存利用率明显提升

👉 这一步是 4MB → 1MB 的关键。


四、防止“拆过头”:experimentalMinChunkSize

拆包之后,我遇到的下一个问题是:

chunk 变多了,但有些只有几 KB

这时候我加了这个配置:

experimentalMinChunkSize: 20 * 1024

它的作用很简单:

小于 20KB 的 chunk,Rollup 会尝试合并

这样可以避免:

  • 请求数暴增
  • JS 被拆得过碎

这是一个非常工程化、但对实际体验很重要的配置。


五、build 阶段的几个关键瘦身开关

拆包只是结构优化,体积能不能再往下掉,还得靠 build 阶段

1️⃣ 使用 terser 做最终压缩

minify: "terser"

相比 esbuild:

  • terser 更激进
  • 最终体积更小

在我这个场景下,对最大 chunk 的体积下降非常明显


2️⃣ 确保 Tree Shaking 真正生效

treeshake: true
format: "es"

但更重要的是代码写法。

比如 lodash:

// ❌ 整包引入,Tree Shaking 无效
import _ from "lodash";

// ✅ 按需引入
import debounce from "lodash/debounce";

很多时候,Tree Shaking 失败并不是工具问题,而是代码问题


六、结果:4MB → 500KB 是怎么来的?

简单总结一下这次体积下降的来源:

  • ❌ 一个超级大 chunk
  • ✅ 多个职责清晰的 chunk
  • ❌ vendor + 业务代码强耦合
  • ✅ 稳定依赖独立缓存

最终:

最大的 JS Chunk 从 4000+ KB 降到了 500+ KB

而且这个结果是:

  • 未压缩体积
  • 不依赖玄学配置
  • 对业务代码几乎无侵入

七、一些容易被忽略的体积“刺客”

在这次优化过程中,还顺手解决了几个问题:

  • lodash 整包引入
  • crypto / decimal / forge 这类库首屏加载
  • 开发期遗留的调试代码

这些都不会让你“一夜瘦身”, 但会持续让 chunk 变大。


最后

这次优化给我最大的一个感受是:

很多性能问题,并不是因为依赖多,而是因为它们被放错了地方。

如果你项目里也有一个:

  • 异常大的 JS 文件
  • 怎么压都压不下去的 chunk

不妨先从 “拆” 开始,而不是盲目换工具。