
Vite 生产构建分包策略手动拆包与自动拆包的对比分析一、为什么需要关注分包策略Vite 的开发模式使用原生 ESM每个文件独立请求天然没有打包问题。但生产构建时Rollup 会将所有模块打包成有限的几个文件。如果不做任何分包配置可能产生两个极端单个 vendor 文件过大首屏加载时阻塞渲染。文件过碎导致 HTTP 请求数过多网络开销增加。一个典型的中型 React 项目未配置分包时的产物结构dist/assets/ index-abc123.js 2.4MB ← 所有业务代码 依赖 index-abc123.css 180KB vendor-def456.js 1.8MB ← 第三方依赖首屏加载需要等待 4.2MB 的 JS 下载和解析。在 4G 网络下这可能需要 5-8 秒。分包的核心理念是按需加载渐进增强。将首屏不必要的内容拆到独立 chunk 中用户访问时再按需加载。flowchart TB A[入口 index.html] -- B[主入口 chunk] B -- C[首屏必需组件 chunk] B -- D[路由懒加载 chunk] D -- E[页面 A chunk] D -- F[页面 B chunk] D -- G[页面 C chunk] subgraph 首屏加载 B C end subgraph 按需加载 E F G end二、手动拆包精细控制手动拆包通过 Rollup 的output.manualChunks配置实现。适合对依赖关系有明确掌控需求的场景。基本配置示例// vite.config.ts import { defineConfig } from vite; import react from vitejs/plugin-react; export default defineConfig({ plugins: [react()], build: { rollupOptions: { output: { manualChunks: { // React 核心 react-vendor: [react, react-dom, react-router-dom], // UI 组件库 antd-vendor: [antd, ant-design/icons], // 图表库 chart-vendor: [echarts, echarts-for-react], // 工具库 utils-vendor: [lodash-es, dayjs, axios], }, }, }, }, });手动拆包的局限很明显。每个新增依赖需要手动维护分组。如果引入了新库但没有更新配置它会被打入主 bundle导致主 bundle 膨胀。更灵活的手动拆包方式使用函数式配置。// vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { manualChunks(id: string) { // node_modules 中的依赖按包名自动分组 if (id.includes(node_modules)) { // React 生态单独分组 if (id.includes(node_modules/react) || id.includes(node_modules/react-dom) || id.includes(node_modules/scheduler)) { return react-vendor; } // antd 单独分组体积大 if (id.includes(node_modules/antd) || id.includes(node_modules/ant-design)) { return antd-vendor; } // 其他依赖合并到公共 vendor return vendor; } // 业务代码按目录自动拆分 if (id.includes(src/pages/)) { const pageName id.split(src/pages/)[1].split(/)[0]; return page-${pageName}; } }, }, }, }, });函数式配置比静态对象配置灵活但仍有维护成本。业务新增页面时需要确认 chunk 命名是否合理。手动拆包的最佳实践在实际项目中手动拆包推荐遵循以下原则体积阈值原则将超过 200KB 的依赖独立拆分。例如echarts约 800KB、antd约 500KB应该独立成 chunk。变更频率原则将频繁更新的业务代码和稳定不变的第三方依赖分开。每次发布时用户只需重新下载业务代码 chunk第三方依赖 chunk 可以从缓存读取。共享依赖原则被 3 个以上页面共同引用的模块应该提取到 common chunk 中避免重复打包。一个实际项目的手动拆包配置示例export default defineConfig({ build: { rollupOptions: { output: { manualChunks(id: string) { // 1. 体积超过 200KB 的依赖独立拆分 if (id.includes(node_modules)) { if (id.includes(echarts)) return echarts-vendor; if (id.includes(antd)) return antd-vendor; if (id.includes(react)) return react-vendor; if (id.includes(monaco-editor)) return editor-vendor; } // 2. 业务代码按路由拆分 if (id.includes(src/pages/)) { const match id.match(/src\/pages\/([^/])/); if (match) return page-${match[1]}; } // 3. 共享工具函数提取到 common if (id.includes(src/utils/) || id.includes(src/hooks/)) { return common; } // 4. 其他归到 vendor if (id.includes(node_modules)) return vendor; }, }, }, }, });这种配置方式在 80 页面的项目中经过验证首屏 gzip 体积控制在 450KB 以内相比默认构建减少了 65% 的加载时间。关键在于根据项目的实际依赖关系和使用场景灵活调整拆分策略而非盲目套用模板配置。三、自动拆包插件化方案自动拆包方案通过社区插件实现无需手动维护分组规则。推荐使用vite-plugin-chunk-split。// vite.config.ts import { defineConfig } from vite; import { chunkSplitPlugin } from vite-plugin-chunk-split; export default defineConfig({ plugins: [ chunkSplitPlugin({ strategy: default, customSplitting: { // 将体积超过阈值的依赖单独拆分 react-vendor: [/node_modules\/react/, /node_modules\/react-dom/], antd-vendor: [/node_modules\/antd/, /node_modules\/ant-design/], echarts-vendor: [/node_modules\/echarts/], }, }), ], });自动拆包的内部策略flowchart TB A[Rollup 构建输出] -- B{分析模块依赖图} B -- C[识别 node_modules 依赖] B -- D[识别共享模块] C -- E{模块体积阈值判断} E --|超过 100KB| F[独立 chunk] E --|低于 100KB| G[合并共享 chunk] D -- H{被引用次数判断} H --|引用 3 次| F H --|引用 3 次| G F -- I[生成命名 chunk] G -- J[生成 common chunk]两种方案的量化对比维度手动拆包自动拆包初始配置成本高需梳理依赖低即装即用持续维护成本高新增依赖需更新低自动分析拆包精度高可精细控制中依赖插件策略chunk 数量稳定性稳定人为控制不稳定依赖变更影响适用规模100 页面的大型应用中小型应用四、实战对比拆包前后数据在一个包含 80 页面的管理后台项目中对比了三种策略的效果。策略 A无分包默认构建dist/assets/ index-7a3b.js 2.8MB (gzip: 820KB) vendor-9c2d.js 1.6MB (gzip: 480KB) 合计: 4.4MB (gzip: 1.3MB)策略 B手动拆包dist/assets/ index-a1b2.js 120KB (gzip: 38KB) ← 业务入口 react-vendor.js 180KB (gzip: 58KB) ← React 核心 antd-vendor.js 420KB (gzip: 125KB) ← 组件库 vendor.js 860KB (gzip: 250KB) ← 其他依赖 page-*.js (按需) ← 各页面路由 首屏合计: 1.58MB (gzip: 471KB)策略 C自动拆包dist/assets/ index-c3d4.js 115KB (gzip: 36KB) react-vendor.js 180KB (gzip: 58KB) antd-vendor.js 420KB (gzip: 125KB) echarts-vendor.js 320KB (gzip: 95KB) common.js 540KB (gzip: 155KB) page-*.js (按需) 首屏合计: 1.58MB (gzip: 469KB)在 Lighthouse 性能测试中的数据对比xychart-beta title 三种策略 Lighthouse 性能对比 x-axis [FCP(秒), LCP(秒), TTI(秒), JS 体积(MB gzip)] y-axis 数值 0 -- 5 bar [3.2, 4.8, 5.5, 1.3] bar [1.8, 2.6, 3.1, 0.47] bar [1.6, 2.4, 2.9, 0.47]手动拆包和自动拆包的首屏性能差距不大FCP 差距 0.2 秒。主要差距体现在维护成本上。第三周新增 5 个依赖后手动拆包需要更新配置并重新验证而自动拆包无需任何操作。但也存在自动拆包失灵的案例。当一个库的多个子包分散在node_modules的不同目录下时自动拆包可能无法正确识别。例如ant-design/icons和ant-design/pro-components可能被分到不同的 chunk 中。此时需要手动干预customSplitting: { antd-full: [ /node_modules\/antd/, /node_modules\/ant-design/, /node_modules\/antv/, ], }五、总结手动拆包和自动拆包并非二选一的关系更适合组合使用。建议策略先用自动拆包插件覆盖 80% 的依赖分组再通过customSplitting手动干预剩余的 20% 特殊情况。对于体积超过 300KB 的依赖如 antd、echarts无论使用哪种策略都应独立拆分为单独 chunk。核心关注指标是首屏 JS 体积而非 chunk 总数。一个合理的首屏 gzip 体积应控制在 500KB 以内。