资讯详情

Rollup 代码分割实战:从动态导入到 manualChunks 优化首屏加载

📅 2026/9/23 9:43:07 | 华诺云谱 👁 阅读
Rollup 代码分割实战:从动态导入到 manualChunks 优化首屏加载
1. 从一个“单文件加载缓慢”问题说起为什么需要代码分割前阵子帮朋友排查一个生产环境的性能问题现象很典型一个后台管理系统首屏白屏时间在弱网环境下能到 8 秒以上。打开浏览器 Network 面板一看主 JS 文件 6.8MB一个文件走完下载、解析、执行整条链路慢得让人怀疑人生。当时项目用的就是 Rollup 打包配置非常简单——单入口、单输出所有业务代码连同 node_modules 里的第三方库全部打成了一个 bundle。这个场景我想很多用 Rollup 的朋友都不陌生。Rollup 的默认行为就是倾向“合并”把 ES Module 的依赖图完整解析之后尽可能把模块合并输出成更少的 chunk。在构建库比如发布一个 npm 包的时候这种单文件输出是合理的选择因为库的消费者更想拿到一个干净、完整、可直接引入的文件。但放到业务项目里尤其是一个持续迭代、依赖越来越多的应用单文件输出就会让首屏加载的代价越来越大用户得等整个应用的代码全部下载完才能看到界面。代码分割Code Splitting解决的核心问题就在于此把代码拆成多个 chunk让用户访问某个页面或触发某个功能时只需要加载对应的那一部分而不是整个应用。Rollup 从 1.0 版本开始就内置了对代码分割的原生支持只是很多朋友只在文档里扫过一眼 Dynamic Import 的用法并没有系统理解它的机制、边界和真正适合自己的拆包姿势。这篇文章我就以 Rollup 为对象把代码分割这件事讲透动态导入是怎么触发分割的、chunk 之间的依赖关系如何处理、多入口和 manualChunks 怎么配合、真实项目里常见的坑有哪些以及 Rollup 和 Webpack、Vite 在这个能力上的设计差异。文章里涉及的操作和配置我都实际跑过后面会尽量用可复现的方式写出来。如果你是刚接触 Rollup 不久建议先搞懂两个基础概念再往下读一是 Rollup 以 ES Module 为第一公民打包过程本质上是构建模块依赖图二是 output.format 对代码分割的行为有硬约束es和esm等 ESM 格式支持天然的分割输出而cjs、iife等格式在分割能力上有明确限制iife 甚至不支持代码分割。这两个前提直接影响后面所有的配置。2. 动态导入Rollup 触发代码分割的核心机制代码分割在 Rollup 里的第一触发方式是动态导入Dynamic Import。也就是在代码里用import()函数语法异步加载模块。Rollup 在解析阶段发现import()调用后会把目标模块及其依赖单独划分成一个或多个 chunk除非你显式关闭这个行为。2.1 动态导入生成 chunk 的完整过程我先搭一个最小项目来演示。目录结构如下├── package.json ├── rollup.config.mjs └── src ├── index.js └── heavy.jssrc/heavy.js故意写一段体积较大的代码模拟一个“只有用到某个功能时才需要加载的模块”// src/heavy.js export const heavyTask (input) { // 这里模拟一个耗时的加密/数据处理过程 const data []; for (let i 0; i 100000; i) { data.push(input i); } return data.reduce((sum, item) sum item, 0); };src/index.js里用动态导入引用它// src/index.js const runHeavyTask async () { const { heavyTask } await import(./heavy.js); return heavyTask(10); }; runHeavyTask().then((result) { console.log(result:, result); });Rollup 配置非常简单// rollup.config.mjs export default { input: src/index.js, output: { dir: dist, format: esm, entryFileNames: [name].js, chunkFileNames: chunks/[name]-[hash].js } };注意这里的输出配置用的是dir而不是file。这是一个关键点只要使用代码分割Rollup 的输出必须指定dir输出目录因为结果是多个文件。如果你沿用单入口时的习惯写output.fileRollup 会直接报错Invalid value file for option output.dir - you must set output.dir instead of output.file when code-splitting.执行npx rollup -c后dist 目录会生成两个文件dist ├── index.js └── chunks/heavy-a1b2c3.jsindex.js里不再是打包进heavy.js内容的完整 bundle而是保留了动态导入的“骨架”// dist/index.js内容已简化 const runHeavyTask async () { const { heavyTask } await import(./chunks/heavy-a1b2c3.js); return heavyTask(10); }; runHeavyTask().then((result) { console.log(result:, result); });这份输出在标准 ESM 环境下可以直接通过静态服务器访问并运行。Rollup 所做的就是把原先的静态依赖关系转换为运行时按需请求的加载链路。2.2 共享依赖怎么拆分chunk 之间会形成依赖图用动态导入做分割后一个容易忽视的问题是如果index.js和heavy.js同时依赖同一个第三方模块Rollup 会怎么处理我改一下示例。新建src/utils.js// src/utils.js export const formatMsg (msg) [app] ${msg};index.js和heavy.js都引入它// src/index.js import { formatMsg } from ./utils.js; const runHeavyTask async () { const { heavyTask } await import(./heavy.js); return heavyTask(10); }; console.log(formatMsg(app started)); runHeavyTask().then((result) { console.log(result:, result); });// src/heavy.js import { formatMsg } from ./utils.js; export const heavyTask (input) { const data []; for (let i 0; i 100000; i) { data.push(input i); } return data.reduce((sum, item) sum item, 0) formatMsg(: heavy done); };再次打包dist 结构变成dist ├── index.js ├── chunks/utils-abcdef.js └── chunks/heavy-123456.jsutils.js被单独拆成了一个 chunkindex.js和heavy.js在运行时都会加载这个公共 chunk。Rollup 这里采用的是 Webpack 类似的做法——把多个 chunk 的共享模块提取出来形成独立 chunk避免同一份代码被重复打进多个文件。这个行为的价值要结合实际场景看。如果两个动态加载的页面模块共享同一个工具模块把工具模块独立出来用户访问页面 A 时浏览器缓存了公共 chunk再访问页面 B 时就不会重复下载这部分代码。如果不提取同一个工具代码会被分别包含在页面 A 和页面 B 的 chunk 里每个页面各自白付一份下载成本。2.3 动态导入的模块“副作用”问题用动态导入时模块顶层的副作用代码side effects执行时机和静态导入有明显差异这个很多人踩过坑。静态import的模块副作用会在父模块执行前同步完成而动态import()内部模块的副作用会在异步请求完成后、模块体内的函数和值暴露之前执行。换句话说// 静态导入utils.js 的顶层代码立刻执行 import ./utils.js; // 动态导入utils.js 的顶层代码在 await 结果返回前才执行 await import(./utils.js);如果你有一个模块在导入时做了初始化工作比如设置全局事件监听、填充某个单例的缓存从静态导入改成动态导入初始化时机就会延后。大多数情况下这只是影响时序但如果你依赖的是“模块加载完成即初始化完成”的假设业务逻辑可能就会出问题。所以做动态导入切分前先问自己一个问题这个模块的顶层副作用是不是可以按需触发如果答案是“不能”那它就不适合动态导入。这也是 Webpack 社区常说的“对具备副作用的基础设施模块避免懒加载”。3. 多入口配置与 manualChunks把分割主动权握在手里动态导入是“代码主动请求分割”而 Rollup 还提供了一条“配置层面强制分割”的路径——多入口和 manualChunks。这两种方式适合不同的场景核心区别在于前者由代码结构驱动后者由打包配置驱动。3.1 多入口Multi-Entry适合 MPA 和库拆包多入口的配置非常简单input可以是对象、数组或者 glob 路径。最常见的用法是传统多页应用MPA每个页面一个独立入口// rollup.config.mjs export default { input: { home: src/pages/home/index.js, detail: src/pages/detail/index.js, admin: src/pages/admin/index.js }, output: { dir: dist, format: esm, entryFileNames: [name]-[hash].js, chunkFileNames: chunks/[name]-[hash].js } };Rollup 会为每个入口生成一个独立的 entry chunk。入口之间共享的模块会按照依赖分析的结果提取成公共 chunk。比如两个页面都引入了utils.js和api.js这两个模块会被打进同一个 shared chunk被两个 entry 分别加载。多入口模式有一个细节值得留意每个入口都是独立的执行起点Rollup 不会假设入口之间有任何关系。所以如果home和admin都依赖同一个包含副作用的模块比如初始化埋点 SDK这个 SDK 的副作用代码就会被提取进公共 chunk执行时执行一次还是多次取决于浏览器加载公共 chunk 的次数。如果页面是独立 HTML各自加载各自的公共 chunk副作用自然是“每页执行一次”这通常是符合预期的但如果你在同一个页面里像 SPA 一样动态注入多个 entry副作用的重复执行就得小心。多入口另一个常见用途是库拆包。如果你在做一个组件库想输出button.js、dialog.js、tabs.js等独立文件让使用方按需引用多入口是比“一个大文件 tree-shaking”更可靠的做法。tree-shaking 依赖使用方的打包器正确识别副作用的标记多入口则直接把“按需”做到了产物层面。3.2 manualChunks手动控制公共库分组动态导入和多入口都解决“代码被拆开”的问题但没有解决“拆得合不合理”的问题。一个典型的痛点项目里有 20 个页面动态加载每个页面都引用了lodash-es或dayjsRollup 默认会把公共模块提取成一个 chunk但如果这些公共模块来自不同性质的库、更新频率不同、浏览器缓存价值不同统统混在一个 chunk 里并不是最优策略。output.manualChunks就是用来在配置层面指定哪些模块应该被手动归入同一个 chunk。它的用法是函数形式// rollup.config.mjs export default { input: src/index.js, output: { dir: dist, format: esm, manualChunks(id, { getModuleInfo, getModuleIds }) { if (id.includes(node_modules)) { if (id.includes(lodash-es) || id.includes(dayjs)) { return vendor-basic; } if (id.includes(echarts) || id.includes(d3)) { return vendor-charts; } // 其他 node_modules 里没匹配到的库 // 如果不 returnRollup 会按默认规则继续处理 } } } };manualChunks回调接收模块的绝对路径id和第二个参数一个包含getModuleInfo、getModuleIds等方法的对象返回值就是自定义 chunk 的名称。返回undefined表示“不管”由 Rollup 的默认逻辑决定返回字符串则强制该模块进入指定 chunk。引入manualChunks后依赖关系会变成每个页面 chunk 引用vendor-basic或vendor-charts这些 vendor chunk 相互独立。好处显而易见lodash-es和dayjs这类底层库基本不变浏览器缓存命中率极高echarts这类大而复杂的图表库和业务代码分开更新业务代码不会让用户重新下载几十兆的图表代码。3.3 manualChunks 使用中容易踩的坑循环调用与公共依赖合并manualChunks在 Rollup 里看起来简单实际用起来有个很隐蔽的坑当你在 manualChunks 里引入“共享模块”时Rollup 可能会把原本已经拆开的入口 chunk 重新合并。举个例子。你有两个入口a.js和b.js分别动态导入各自页面但两个页面都用shared.js。你希望shared.js单独成 chunk。但如果不小心在 manualChunks 里把shared.js的父级比如a-page.js和b-page.js都依赖的一个容器组件也指定进了同一个 chunkRollup 可能为了满足“a-page 和 b-page 都要引入该 chunk”的条件生成一个巨大的公共 chunk或者反过来把整个依赖图拆成不符合预期的形状。这个问题的根源在于Rollup 的代码分割和 Webpack 不同它没有“按运行时入口动态分发”的分包机制而是完全基于静态模块图。manualChunks 的返回值本质上是在告诉 Rollup“这些模块必须在一起”Rollup 为了满足这个约束会调整 chunk 划分甚至可能膨胀公共 chunk。实际操作中我的建议是manualChunks 只用于“node_modules 里的稳定第三方库”这种边界清晰、依赖关系简单的模块。业务代码模块尽量靠动态导入的默认机制去分割不要在手写 manualChunks 时把业务模块和第三方模块混在一起分组。4. 真实项目里的分割策略与体积权衡理论讲完落到真实项目上代码分割策略的选择其实是在三个目标之间找平衡首屏体积加载速度、缓存复用二次访问体验、请求数量HTTP/2 下可以放宽但 HTTP/1.1 下仍是硬约束。4.1 按路由拆最常见的业务分割模式SPA 应用里最天然的分割单位是路由。每个路由对应一个页面组件只有访问该路由时才加载对应代码。Rollup 下实现方式就是动态导入路由组件配合路由库的懒加载配置。如果你用的是 React 和 React Router代码大致是这样// src/router.jsx import { createBrowserRouter } from react-router-dom; import { lazy, Suspense } from react; const HomePage lazy(() import(./pages/Home)); const DetailPage lazy(() import(./pages/Detail)); const AdminPage lazy(() import(./pages/Admin)); const router createBrowserRouter([ { path: /, element: Suspense fallback{divloading.../div}HomePage //Suspense }, { path: /detail/:id, element: Suspense fallback{divloading.../div}DetailPage //Suspense }, { path: /admin, element: Suspense fallback{divloading.../div}AdminPage //Suspense } ]);这套代码配合 Vite底层构建用 Rollup打包后默认行为就是一个路由一个 chunk。你需要注意的配置点是rollupOptions.output.chunkSizeWarningLimit// vite.config.ts export default { build: { rollupOptions: { output: { chunkSizeWarningLimit: 500, // 默认是 500 KB manualChunks: { // 把 React 全家桶单独分出来 vendor-react: [react, react-dom, react-router-dom] } } } } };chunkSizeWarningLimit本身不改任何打包行为只是控制警告阈值。如果你不加 manualChunksRollup 很可能因为“首屏页面 chunk 超过 500KB”而对你哐哐报警告。噪声太大警告也就失去警示意义了。4.2 第三方库怎么拆按更新频率和体积决定第三方库的拆包策略我一般在真实项目里按三个维度排序体积大小、更新频率、被页面依赖的广泛度。大体积且低频更新如 echarts、monaco-editor、antd必须单独拆最好还能按子模块进一步拆比如 echarts 按图表类型动态导入。这类库不进主 chunk业务更新永远不触发它们的重新下载。小体积且低频更新如 dayjs、lodash-es、axios可以合并成一个 vendor chunk。单独为每个小库拆一个 chunk 会导致请求数过多收益却不明显。把这些小库合在一起chunk 体积也就几十 KB缓存策略等同于一个大块简单有效。体积小且高频更新内部工具函数、业务公共组件不应该被打进 vendor chunk否则会让“低频更新”的 vendor 块失去缓存价值。这类模块让 Rollup 默认提取成业务公共 chunk 就行或者在 manualChunks 里和第三方库分开。个人常用的第三方库分组方案manualChunks(id) { if (!id.includes(node_modules)) return; if (id.includes(echarts) || id.includes(zrender)) return echarts; if (id.includes(monaco-editor)) return monaco; if (id.includes(antd) || id.includes(ant-design)) return antd; // 剩余 node_modules 里的库统一放 vendor return vendor; }注意antd和echarts这种大型库内部还有自己的动态导入逻辑你手动把它们归进一个 chunk 后Rollup 会尽量合并。如果echarts模块里有大量动态分割点manualChunks 强制合并后最终产物可能会因为循环依赖或模块共享产生额外的公共 chunk——这属于正常现象不用过度恐慌。4.3 拆得太碎的教训chunk 爆炸问题滚动发布后总有一种反向优化的冲动把每个页面、每个组件甚至每个工具函数都拆成独立 chunk。但这在 HTTP/1.1 时代是灾难在 HTTP/2 时代也要谨慎。HTTP/2 支持多路复用请求数量不再是严格瓶颈但每个请求依然有 header 开销、TLS 握手后的加解密开销而且浏览器对同一域的并发请求数仍有上限各家不同通常 6~8 个。如果一个页面首屏依赖 30 个 chunk即使 HTTP/2 能并发下载浏览器解析、编译、执行这些小型 ESM 模块的开销也非常可观。Rollup 输出的 ESM chunk 之间是原生 import 关系浏览器必须等待 import 链上的所有模块下载完成才能执行入口代码。这里有一个很反直觉的性能细节ESM 的解析是“深度优先”的浏览器会先 fetch 所有静态 import 的模块再开始执行。所以 chunk 数量也不是越少越好——太小太多会导致网络往返和解析开销放大太大则会拖慢首屏。这才是代码分割真正的“权衡的艺术”。一个务实原则业务页面按路由拆第三方库按“大库独立、小库合并”拆业务公共代码尽最大可能复用单个公共 chunk。不要为了“拆”而拆。5. 踩坑实记循环依赖、过碎 chunk 与加载时序问题排查代码分割在简单项目里跑得很顺但到了依赖关系复杂的项目里问题会一个接一个冒出来。这一节记录几个我实际遇到过、也排查过的问题排查思路比最终答案更有参考价值。5.1 问题一动态导入后模块的循环依赖导致运行时 undefined现象是某个模块从静态导入改成动态导入后页面正常渲染但点击某个按钮时报TypeError: Cannot read properties of undefined (reading xxx)。追了半个下午发现是循环依赖。场景简化如下a.js import b.js b.js import c.js c.js import a.js 形成循环原先静态导入时Rollup 会尽量把这三个模块合并进同一个 chunk循环引用在同一个模块作用域里通过“提升”的方式互相引用运行顺序碰巧没问题。改成动态导入a.js后a.js被拆成独立 chunk而a.js依赖的b.js、c.js在另一 chunk 里。动态 chunk 加载完成后模块初始化顺序发生了变化c.js里访问a.js导出的变量时该变量还是undefined。这类问题常规修复方案有两个一是调整代码消除循环依赖最优解二是把循环依赖涉及的模块通过 manualChunks 强制放回同一个 chunk让 Rollup 用合并方式规避初始化顺序问题治标。业务代码里尽量还是治本——循环依赖在代码分割场景下会从“不报错”变成“神秘报错”排查成本很高。排查这类问题的工具建议Chrome DevTools 的 Sources 面板里看 Network 请求顺序和 console 报错的 stack trace报错位置通常在“模块顶层代码里引用了尚未初始化的变量”这一块而不是在函数调用里。一旦 stack trace 里出现模块初始化阶段不是函数执行阶段就可以大胆往循环依赖方向排查了。5.2 问题二chunk 数量爆炸导致构建产物目录难以管理有朋友反馈项目构建后 dist 目录有几百个 chunk 文件构建时间暴涨产物部署也慢。看他的配置每个页面都用动态导入页面里每个组件又各自动态导入连一个简单的弹窗组件都用lazy(() import(./Modal))包裹。解决方案不是修改单个配置而是需要重新审视分割粒度动态导入的单元应该是“功能模块”或“路由页面”而不是“组件”。组件级别的懒加载适合那种“体积大、展示少见、仅在用户特定交互后才出现”的重组件比如富文本编辑器、图表组件。普通弹窗、普通表单组件拆成独立 chunk只会徒增请求数。Rollup 对过碎 chunk 的官方回应是experimentalMinChunkSizeRollup 4 新增。它允许设置一个最小 chunk 体积低于阈值的 chunk 会被尝试合并进其他 chunkoutput: { experimentalMinChunkSize: 1000 // 单位字节 }注意这个 API 名称带了experimental前缀用法和稳定性随着 Rollup 版本变化可能不同用之前先查看当前版本对应文档。它做的是“合并过小的 chunk”而不是“撤销代码分割”所以不会把整个页面 chunk 都吞回去安全度相对可控。5.3 问题三动态加载后页面白屏与“loading 闪烁”代码分割后最常见也最容易忽略的是加载时序问题。动态导入是异步加载在请求完成前页面处在“空白”状态。如果懒加载的 chunk 很大、网速又慢用户会看到一个长时间无反馈的页面。处理这个问题的标准姿势是使用 SuspenseReact或路由级 loading 状态。不过更底层的优化是给首屏用的动态 chunk 做 preload 或 prefetch 提示让它和入口 chunk 并行下载而不是等入口执行完、解析到import()调用时才去请求。Rollup 这边不直接提供 preload 标签生成能力但结合构建产物可以在 HTML 里手动加link relpreload href/assets/home-abc123.js asscript /不过手写 hash 文件名在每次构建后都会变维护成本高。现实项目里Vite 这种集成方案会自动为入口 chunk 生成link relmodulepreload对首屏关键动态 chunk 建议你在应用代码层用“自动注入”的方式处理或者直接接受“入口 chunk 小、动态 chunk 按需加载”的默认行为把精力花在缩小具体页面 chunk 的体积上。5.4 问题四动态导入与公共依赖反复匹配导致的 chunk 循环加载这是比较深的一个坑。当我用manualChunks把echarts强制分组后Rollup 偶尔会生成一个同时被主页 chunk 和图表页 chunk 依赖的“额外公共 chunk”并且这个额外 chunk 反过来依赖echartschunk表面上看起来像是循环 chunk 引用。实际排查后发现并没有真正的循环而是echarts内部有一部分代码是动态导入加载的子模块这些子模块与业务代码共享了某些小工具模块。当我把echarts全部塞进一个 chunk 后那些动态导入的子模块也被合并了导致额外公共 chunk 必须同时被业务 chunk 和echartschunk 引用引用关系形成“伪循环”。解法是放弃对echarts的强制分组允许它的内部拆包按默认规则走或者只把echarts的核心入口echarts/core做 manualChunks内部更细分的按需模块由 Rollup 自行处理。一句话manualChunks 的目标越“大而全”越容易触发 chunk 重组的不可控行为。6. Rollup、Vite 与 Webpack 在代码分割上的设计差异讨论 Rollup 代码分割绕不开和 Webpack 的对比以及 Vite 在底层使用 Rollup 之后做了哪些增强。6.1 Rollup 与 Webpack 的分割范式差异Webpack 的代码分割逻辑建立在“运行时 runtime chunk graph”基础上。Webpack 在产物里注入一段运行时加载器负责模块映射、懒加载、公共 chunk 的运行时装配。也就是说Webpack 的产物不是浏览器原生 ESM而是一套自己实现的模块运行时系统。这套系统功能强大支持各种 magic comment、动态 chunk 命名、preload/prefetch 声明但产物里会有一段不菲的 runtime 代码。Rollup 的代码分割则完全基于原生 ES Module产物就是标准 ESM 文件chunk 之间通过原生import语句互相引用。浏览器本身充当模块加载器和运行时。这意味着 Rollup 的产物更干净、更符合浏览器原生机制不需要额外 runtime 代码但代价是所有依赖原生 ESM 支持的浏览器环境才能运行。在现代浏览器里这就够用了但在需要兼容老浏览器的项目里Rollup 的 ESM 产物还得通过额外的构建链改造。Webpack 还有一个 Rollup 没有的能力是splitChunks的cacheGroups弹性分组策略。它可以在“自动提取公共依赖”和“手动分组”之间做非常精细的规则配置而 Rollup 的manualChunks更像一把“硬编码”的锤子灵活度低一档。不过 Rollup 4 的experimentalMinChunkSize和持续演进的 chunk 生成规则在缩小差距。6.2 Vite 站在 Rollup 肩膀上做了什么Vite 生产构建用 Rollup开发环境用 esbuild 做依赖预构建。这套组合的关键在于开发环境根本不打包代码浏览器直接请求 ESM 源码生产环境才让 Rollup 做完整的代码分割和压缩。Vite 给 Rollup 代码分割加了几个实用增强最明显的是build.rollupOptions.output.manualChunks的使用体验它把 Rollup 的函数手动分组和 Vite 的项目结构做了融合。Vite 还默认开启了对node_modules的自动 vendor 拆分并且提供build.modulePreload.polyfill等选项帮开发者处理 modulepreload 的兼容问题。实际开发中在 Vite 项目里做代码分割我建议重点掌握两个配置项// vite.config.ts export default { build: { rollupOptions: { output: { inlineDynamicImports: false, // 保持动态导入拆包不要合并 manualChunks(id, { getModuleInfo }) { ... }, } }, modulePreload: { polyfill: true, // 老浏览器自动注入 modulepreload polyfill } } };inlineDynamicImports是个危险选项默认false。一旦显式设为true所有动态导入会被强制内联成一个 bundle代码分割完全失效。有些朋友为了让某些环境比如 service worker 或 localStorage 缓存场景拿到单一文件会打开它但这基本等于放弃代码分割收益。能不开就别开。6.3 选型建议什么时候用 Rollup什么时候别强求代码分割最后回到一个现实问题什么项目适合用 Rollup 做代码分割我的经验是发布 npm 库时优先用 Rollup 单文件输出业务应用如果主要跑在现代浏览器、希望产物更纯净、追求更快的首屏加载Rollup或 Vite 底层 Rollup是很好的选择如果需要兼容老浏览器或者对 chunk 分组的精细控制有强烈诉求Webpack 的生态和配置灵活度仍然是杀手锏。代码分割不是构建工具领域的“屠龙之术”本质上它是体积管理、网络策略、浏览器缓存三者之间的匹配工具。Rollup 的代码分割机制非常简单直接——动态导入触发拆包、配置与手动分组做调优、产物是原生 ESM——理解这条链路你在真实项目里无论遇到拆包过细、过粗还是循环依赖问题都能快速定位到正确方向。我个人在实际项目中最深刻的体会是先明确自己的分割目标再动手配置而不是配置完再去看效果。你要先知道“我希望首屏体积压到多少”“我期望哪些代码可以长期缓存”“用户第二次访问时哪些模块可以直接命中缓存”然后按这几个目标去设计动态导入边界和 manualChunks 分组。目标不清配置就只是在碰运气。最后分享一个小技巧每次调整分割配置后不要只看构建日志里的体积数字真正用浏览器 DevTools 的 Network 面板跑一次首屏加载观察请求瀑布图看有没有非预期的串行请求和过大的单文件。代码分割的价值最终体现在“用户的加载体验曲线”上而不是“构建产物里有多少个 chunk”这个表面指标上。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。