Vite 生产构建的 Rollup 配置调优:插件顺序、输出格式与资源内联
一、Vite 生产构建的底层架构
Vite 的生产构建本质上是 Rollup 的一次受控执行。Vite 在 Rollup 之上叠加了两层封装:第一层是插件兼容层(将 Vite 插件格式转换为 Rollup 插件接口),第二层是配置聚合层(将vite.config.ts中的分散配置项映射到 Rollup 的OutputOptions、InputOptions等结构)。
理解这个映射关系是调优的前提。很多开发者直接修改vite.config.ts的build.rollupOptions字段,却不知道 Vite 内部已经注入了隐含默认值——比如output.format默认为esm,output.manualChunks默认使用 Vite 的自动分割策略。盲目覆盖可能导致内建优化失效。
具体而言,配置流程始于vite.config.ts,经由 Vite 配置聚合层处理后,分别映射为 Rollup 的InputOptions(负责插件排序与入口定义)和OutputOptions(负责输出格式与分块策略)。这两者共同驱动 Rollup 执行,最终生成 ESM/CJS/IIFE 格式的产物及静态资源。
二、插件顺序:构建链路的性能瓶颈
Rollup 插件按注册顺序执行,钩子分为build阶段(解析、转换)和output阶段(生成、写入)。插件顺序直接影响两个指标:构建耗时与产物体积。
2.1 构建阶段插件的排序原则
// vite.config.ts — 正确的插件排序示范 import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react';import legacy from '@vitejs/plugin-legacy';
import compression from 'vite-plugin-compression';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
// 1. 语法转换类插件——最先执行,确保后续插件能处理标准 AST
react(),
// 2. 分析类插件——在转换后执行,读取标准化 AST visualizer({ open: false, gzipSize: true }), // 3. 产物后处理类插件——最后执行 legacy({ targets: ['defaults', 'not IE 11'] }), compression({ algorithm: 'gzip', threshold: 10240 }),],
build: {
rollupOptions: {
// 以下配置直接映射到 Rollup OutputOptions
output: {
format: 'esm',
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
},
});
排序原则总结: - **语法转换优先**(babel/swc/ts):后续插件依赖标准化 AST - **代码分析居中**(visualizer/coverage):需要读取最终 AST - **产物后处理末尾**(compression/legacy):操作最终输出 错误排序的典型后果:将 compression 放在 react 之前,会导致 JSX 未转换就被压缩,产物不可用。 ### 2.2 插件钩子冲突检测 ```typescript /** * 检测插件钩子注册顺序冲突 * build 钩子必须先于 output 钩子 */ function validatePluginOrder(plugins: Plugin[]): string[] { const warnings: string[] = []; const buildHooks = ['resolveId', 'load', 'transform']; const outputHooks = ['renderChunk', 'writeBundle', 'generateBundle']; let lastBuildIndex = -1; plugins.forEach((plugin, index) => { const hasBuild = Object.keys(plugin).some(k => buildHooks.includes(k)); const hasOutput = Object.keys(plugin).some(k => outputHooks.includes(k)); if (hasOutput && !hasBuild && index < lastBuildIndex) { warnings.push( `插件 "${plugin.name}" 仅含 output 钩子却位于 build 钩子插件之前,建议调整顺序` ); } if (hasBuild) { lastBuildIndex = index; } }); return warnings; }三、输出格式选择与多格式配置
3.1 三种输出格式的适用场景
| 格式 | 适用场景 | Tree-shaking | 动态导入 | 浏览器直接加载 |
|---|---|---|---|---|
| ESM | 现代浏览器、构建工具链消费 | 完整支持 | 完整支持 | 支持 |
| CJS | Node.js 服务端、旧版工具 | 部分支持 | 需 polyfill | 不支持 |
| IIFE | CDN 直接引入、非构建场景 | 不支持 | 不支持 | 支持 |
3.2 多格式输出配置
当库需要同时支持 ESM 和 CJS 消费者时:
import { defineConfig } from 'vite'; export default defineConfig({ build: { lib: { entry: 'src/index.ts', name: 'MyLib', fileName: (format) => `my-lib.${format}.js`, }, rollupOptions: { // 多格式输出:每个格式独立配置 output: [ { format: 'esm', // ESM 输出保留动态导入,不做 polyfill preserveModules: true, preserveModulesStructure: true, entryFileNames: '[name].mjs', }, { format: 'cjs', // CJS 输出需处理动态导入为同步 require dynamicImportInCjs: true, entryFileNames: '[name].cjs', }, ], // 外部化 Node.js 内置模块 external: ['fs', 'path', 'crypto'], }, }, });注意:preserveModules: true与manualChunks互斥。前者按源文件结构分割模块,后者按自定义逻辑合并——两者不能同时生效。
四、资源内联策略:权衡请求数与首屏体积
4.1 小资源内联的阈值决策
CSS 小文件、SVG 图标、JSON 配置等小型资源可以内联到 JS bundle 中,减少 HTTP 请求。但内联会增加 JS 体积,阻塞主线程解析。
/** * 资源内联阈值计算器 * 根据资源体积与当前 bundle 大小动态决策 */ function calculateInlineThreshold( resourceSize: number, bundleSize: number, httpCostMs: number = 50 // 单次额外请求的延迟估算 ): { shouldInline: boolean; reason: string } { // 内联后 JS 解析延迟增加估算:1KB ≈ 1ms(移动端) const parseCostMs = resourceSize / 1024; const inlineOverheadMs = parseCostMs; // 外联的额外 HTTP 请求成本 const externalCostMs = httpCostMs; if (resourceSize < 4096) { // 小于 4KB:内联收益明确 return { shouldInline: true, reason: '体积 < 4KB,请求节省优先' }; } if (inlineOverheadMs < externalCostMs) { return { shouldInline: true, reason: `内联解析延迟 ${inlineOverheadMs}ms < 外联请求延迟 ${externalCostMs}ms` }; } return { shouldInline: false, reason: `内联解析延迟 ${inlineOverheadMs}ms > 外联请求延迟 ${externalCostMs}ms` }; }4.2 Vite 内联配置实践
// vite.config.ts — 资源内联精细化配置 export default defineConfig({ build: { // 小于 4KB 的资源内联为 base64/data URI assetsInlineLimit: 4096, // CSS 内联策略:开发环境外联(便于 HMR),生产环境内联 cssCodeSplit: true, rollupOptions: { output: { // 手动分块:将 runtime 与业务代码分离 manualChunks(id) { if (id.includes('node_modules')) { // 第三方依赖按包名分块 const match = id.match(/node_modules\/([^/]+)/); if (match) { const pkgName = match[1]; // 大型框架单独分块 if (['react', 'react-dom', 'vue'].includes(pkgName)) { return `vendor-${pkgName}`; } return 'vendor-misc'; } } }, }, }, }, });五、总结
Vite 生产构建调优的核心认知是:Vite 并非"零配置即可用"的黑箱,而是 Rollup 的受控封装。调优的三个关键维度:
- 插件顺序:语法转换 → 代码分析 → 产物后处理,逆序会导致构建失败或产物异常
- 输出格式:ESM 优先(tree-shaking 完整),CJS 兼容(服务端消费),IIFE 仅用于 CDN 直接引入
- 资源内联:4KB 阈值是经验起点,实际决策应对比内联解析延迟与外联请求延迟
配置调优不是一次性动作。随着依赖版本升级、业务模块增长,分块策略和内联阈值需要定期重新评估。rollup-plugin-visualizer 是持续监控产物体积的必备工具。