大型 SaaS 产品的 Vite 迁移实录从 Webpack 到 Vite 的 6 个月演进一、迁移背景与前期评估该项目为面向企业客户的 SaaS 平台前端仓库包含 32 个子应用微前端架构总模块数超过 6800 个。技术栈为 React 18 TypeScript Less构建工具使用 Webpack 5构建产线为 Jenkins Docker。迁移前的构建痛点开发服务器冷启动单应用 45-90s32 个应用全量启动需约 18 分钟。HMR 延迟修改一行代码到浏览器热更新平均等待 3.2s。生产构建耗时全量构建约 14.5 分钟CI 流水线的等待时间成为交付瓶颈。配置复杂度Webpack 配置文件总量超过 3200 行包含 18 个自定义 loader 和 24 个 plugin。经过两周的技术评估确定 Vite 迁移的可行性项目以 ESM 为主TypeScript 源码核心依赖React、Ant Design、ECharts均提供 ESM 版本不存在不可绕过的 Webpack 特有功能依赖。二、基础迁移配置文件对齐2.1 resolve.alias 映射Webpack 中大量使用了别名指向src/目录Vite 中通过resolve.alias等价的配置/** * Vite 配置文件 * * 与该项目的 Webpack 配置功能等价 * 保留所有别名映射以确保导入路径不发生变化。 */ import { defineConfig } from vite; import react from vitejs/plugin-react; import path from node:path; export default defineConfig({ plugins: [ react({ // 启用 babel 以兼容部分装饰器语法 babel: { plugins: [ [babel/plugin-proposal-decorators, { legacy: true }], ], }, }), ], resolve: { alias: { : path.resolve(__dirname, src), components: path.resolve(__dirname, src/components), utils: path.resolve(__dirname, src/utils), hooks: path.resolve(__dirname, src/hooks), services: path.resolve(__dirname, src/services), types: path.resolve(__dirname, src/types), // 保留 Webpack 时期的公共组件别名 shared: path.resolve(__dirname, ../shared), }, }, css: { preprocessorOptions: { less: { // 注入全局 Less 变量等价于 Webpack 的 style-resources-loader additionalData: import /styles/variables.less; import /styles/mixins.less; , javascriptEnabled: true, }, }, modules: { // CSS Modules 命名规则与 Webpack 保持一致 localsConvention: camelCaseOnly, generateScopedName: [name]__[local]___[hash:base64:5], }, }, });2.2 环境变量兼容Webpack 通过process.env.XXX注入环境变量Vite 使用import.meta.env.XXX。迁移阶段采用兼容性中间层/** * 环境变量兼容层 * * 统一提供 process.env 的访问方式 * 平滑过渡到 import.meta.env减少业务代码改动。 * * 使用方式在入口文件最顶部引入 * import ./env-compat; */ // src/env-compat.ts if (typeof process undefined || !process.env) { (globalThis as Recordstring, unknown).process { env: {} as Recordstring, string, }; } // 将 Vite 环境变量映射到 process.env 上 const envKeys Object.keys(import.meta.env); for (const key of envKeys) { if (key.startsWith(VITE_)) { // 移除 VITE_ 前缀以保持与 Webpack 时期一致 const legacyKey key.replace(/^VITE_/, ); (process.env as Recordstring, string)[legacyKey] ( import.meta.env as Recordstring, string )[key]; } }2.3 require 语法的处理项目中存量代码中存在require.context和动态require的情况。对于自动化加载场景如自动注册全局组件、自动导入路由使用import.meta.glob替代/** * Webpack require.context → Vite import.meta.glob 迁移 * * 原始代码Webpack: * const modules require.context(./modules, true, /\.tsx$/); * modules.keys().forEach(key { ... }); * * 迁移后Vite: */ const modules import.meta.glob{ default: React.ComponentType }( ./modules/**/*.tsx, { eager: true } ); // 保持与原有 API 一致的使用方式 for (const [path, module] of Object.entries(modules)) { const componentName path .replace(./modules/, ) .replace(/\.tsx$/, ) .replace(/\//g, _); registerComponent(componentName, module.default); }三、深度适配自定义 Vite 插件3.1 微前端子应用的构建适配项目基于 qiankun 的微前端架构子应用需要导出bootstrap、mount、unmount生命周期。Vite 的默认构建产物格式为 ESM而 qiankun 需要通过window全局访问子应用因此需要自定义构建配置/** * Vite 插件微前端子应用构建适配 * * 确保构建产物符合 qiankun 的加载要求 * 1. 格式为 UMD通过 window 导出 * 2. 入口 JS 和 CSS 文件名可预测用于主应用动态加载 * 3. publicPath 在运行时动态注入 */ import type { Plugin } from vite; interface MicroAppPluginOptions { /** 子应用名称用于 window 挂载 */ appName: string; /** 构建目标默认 es2015 */ target?: string; } export function microAppPlugin(options: MicroAppPluginOptions): Plugin { const { appName, target es2015 } options; return { name: vite-plugin-micro-app, config(config) { return { ...config, base: //cdn.example.com/micro-apps/${appName}/, build: { ...config.build, target, // 类库模式构建以 UMD 格式暴露 lib: { entry: src/index.tsx, name: appName, formats: [umd], fileName: () index.js, }, rollupOptions: { // 排除主应用提供的公共依赖 external: [react, react-dom, antd, moment], output: { globals: { react: React, react-dom: ReactDOM, antd: antd, moment: moment, }, assetFileNames: index.[ext], }, }, }, }; }, // 在 HTML 中注入 publicPath 动态设置逻辑 transformIndexHtml(html) { return html.replace( /head, script // 动态设置 publicPath支持不同环境部署 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } /script/head ); }, }; }3.2 SVG 图标组件的兼容原 Webpack 配置中使用svgr/webpack将 SVG 文件作为 React 组件导入。Vite 中对应使用vite-plugin-svgr插件确保导入方式完全一致。3.3 传统构建产物的兼容处理项目中有 3 个老旧依赖使用 AMD 格式且不提供 ESM 版本。通过编写 Vite 插件在transform阶段做代码转换将 AMD 的define包装转换为 ESM 格式。四、性能对比与优化4.1 核心指标对比指标Webpack 5Vite提升幅度开发服务器启动单应用68s2.1s97%HMR 响应延迟3.2s62ms98%生产构建单应用4.5min1.8min60%CI 构建32 应用并行14.5min4.2min71%首屏 JS 体积gzip487KB412KB15%4.2 拆包优化策略Vite/Rollup 的默认拆包策略比较保守针对大型 SaaS 应用需要手动配置拆包策略以获得更优的缓存命中率/** * 自定义拆包策略 * * 目标将第三方依赖按更新频率分层 * 最大化浏览器缓存利用率。 */ // vite.config.ts 的 build.rollupOptions.output.manualChunks manualChunks(id: string) { // 框架层React 生态更新频率最低 if (id.includes(node_modules/react) || id.includes(node_modules/react-dom) || id.includes(node_modules/react-router)) { return framework; } // UI 层Ant Design中等更新频率 if (id.includes(node_modules/antd) || id.includes(node_modules/ant-design)) { return antd; } // 图表层ECharts体积大但更新频率低 if (id.includes(node_modules/echarts) || id.includes(node_modules/zrender)) { return echarts; } // 工具层lodash/moment/dayjs更新频率较低 if (id.includes(node_modules/lodash) || id.includes(node_modules/moment) || id.includes(node_modules/dayjs)) { return utils; } // 业务公共代码体积适中与业务迭代同步更新 if (id.includes(src/shared) || id.includes(src/common)) { return common; } // 其余第三方依赖 if (id.includes(node_modules)) { return vendor; } }4.3 迁移中的意外发现迁移完成后的一次 Code Review 中发现 Webpack 时期的ts-loader配置中transpileOnly: true开启了但对应的fork-ts-checker-webpack-plugin却在某次升级中意外失效。这意味着项目在过去 4 个月中CI 没有执行完整的类型检查。切换到 Vite 后团队同时引入vite-plugin-checker确保类型检查在开发和 CI 阶段始终有效。这一问题也直接促成了 CI 流水线中增加独立的tsc --noEmit检查步骤。五、总结六个月、32 个子应用、6800 模块的迁移技术决策的核心经验渐进式迁移优先先迁移一个中等复杂度的子应用作为样板积累配置模板和踩坑经验后再推广避免全面铺开导致的风险。兼容性优先于彻底性环境变量兼容层、别名映射等过渡代码在迁移阶段是必要的。彻底废弃旧模式应该安排在迁移稳定后作为独立迭代进行。迁移是质量检查的机会在迁移过程中发现的类型检查缺失、废弃依赖等问题应当作为迁移任务的一部分一并解决。灰度发布不可省略通过特性开关Feature Flag控制新旧构建产物的下发比例先覆盖内部用户逐步扩大到外网全量是风险最低的切换方式。在 Vite 6基于 Rolldown正式稳定后还可以考虑进一步迁移到 Rolldown 获得更快的生产构建速度。这将是下一个迭代周期的话题。
大型 SaaS 产品的 Vite 迁移实录:从 Webpack 到 Vite 的 6 个月演进
大型 SaaS 产品的 Vite 迁移实录从 Webpack 到 Vite 的 6 个月演进一、迁移背景与前期评估该项目为面向企业客户的 SaaS 平台前端仓库包含 32 个子应用微前端架构总模块数超过 6800 个。技术栈为 React 18 TypeScript Less构建工具使用 Webpack 5构建产线为 Jenkins Docker。迁移前的构建痛点开发服务器冷启动单应用 45-90s32 个应用全量启动需约 18 分钟。HMR 延迟修改一行代码到浏览器热更新平均等待 3.2s。生产构建耗时全量构建约 14.5 分钟CI 流水线的等待时间成为交付瓶颈。配置复杂度Webpack 配置文件总量超过 3200 行包含 18 个自定义 loader 和 24 个 plugin。经过两周的技术评估确定 Vite 迁移的可行性项目以 ESM 为主TypeScript 源码核心依赖React、Ant Design、ECharts均提供 ESM 版本不存在不可绕过的 Webpack 特有功能依赖。二、基础迁移配置文件对齐2.1 resolve.alias 映射Webpack 中大量使用了别名指向src/目录Vite 中通过resolve.alias等价的配置/** * Vite 配置文件 * * 与该项目的 Webpack 配置功能等价 * 保留所有别名映射以确保导入路径不发生变化。 */ import { defineConfig } from vite; import react from vitejs/plugin-react; import path from node:path; export default defineConfig({ plugins: [ react({ // 启用 babel 以兼容部分装饰器语法 babel: { plugins: [ [babel/plugin-proposal-decorators, { legacy: true }], ], }, }), ], resolve: { alias: { : path.resolve(__dirname, src), components: path.resolve(__dirname, src/components), utils: path.resolve(__dirname, src/utils), hooks: path.resolve(__dirname, src/hooks), services: path.resolve(__dirname, src/services), types: path.resolve(__dirname, src/types), // 保留 Webpack 时期的公共组件别名 shared: path.resolve(__dirname, ../shared), }, }, css: { preprocessorOptions: { less: { // 注入全局 Less 变量等价于 Webpack 的 style-resources-loader additionalData: import /styles/variables.less; import /styles/mixins.less; , javascriptEnabled: true, }, }, modules: { // CSS Modules 命名规则与 Webpack 保持一致 localsConvention: camelCaseOnly, generateScopedName: [name]__[local]___[hash:base64:5], }, }, });2.2 环境变量兼容Webpack 通过process.env.XXX注入环境变量Vite 使用import.meta.env.XXX。迁移阶段采用兼容性中间层/** * 环境变量兼容层 * * 统一提供 process.env 的访问方式 * 平滑过渡到 import.meta.env减少业务代码改动。 * * 使用方式在入口文件最顶部引入 * import ./env-compat; */ // src/env-compat.ts if (typeof process undefined || !process.env) { (globalThis as Recordstring, unknown).process { env: {} as Recordstring, string, }; } // 将 Vite 环境变量映射到 process.env 上 const envKeys Object.keys(import.meta.env); for (const key of envKeys) { if (key.startsWith(VITE_)) { // 移除 VITE_ 前缀以保持与 Webpack 时期一致 const legacyKey key.replace(/^VITE_/, ); (process.env as Recordstring, string)[legacyKey] ( import.meta.env as Recordstring, string )[key]; } }2.3 require 语法的处理项目中存量代码中存在require.context和动态require的情况。对于自动化加载场景如自动注册全局组件、自动导入路由使用import.meta.glob替代/** * Webpack require.context → Vite import.meta.glob 迁移 * * 原始代码Webpack: * const modules require.context(./modules, true, /\.tsx$/); * modules.keys().forEach(key { ... }); * * 迁移后Vite: */ const modules import.meta.glob{ default: React.ComponentType }( ./modules/**/*.tsx, { eager: true } ); // 保持与原有 API 一致的使用方式 for (const [path, module] of Object.entries(modules)) { const componentName path .replace(./modules/, ) .replace(/\.tsx$/, ) .replace(/\//g, _); registerComponent(componentName, module.default); }三、深度适配自定义 Vite 插件3.1 微前端子应用的构建适配项目基于 qiankun 的微前端架构子应用需要导出bootstrap、mount、unmount生命周期。Vite 的默认构建产物格式为 ESM而 qiankun 需要通过window全局访问子应用因此需要自定义构建配置/** * Vite 插件微前端子应用构建适配 * * 确保构建产物符合 qiankun 的加载要求 * 1. 格式为 UMD通过 window 导出 * 2. 入口 JS 和 CSS 文件名可预测用于主应用动态加载 * 3. publicPath 在运行时动态注入 */ import type { Plugin } from vite; interface MicroAppPluginOptions { /** 子应用名称用于 window 挂载 */ appName: string; /** 构建目标默认 es2015 */ target?: string; } export function microAppPlugin(options: MicroAppPluginOptions): Plugin { const { appName, target es2015 } options; return { name: vite-plugin-micro-app, config(config) { return { ...config, base: //cdn.example.com/micro-apps/${appName}/, build: { ...config.build, target, // 类库模式构建以 UMD 格式暴露 lib: { entry: src/index.tsx, name: appName, formats: [umd], fileName: () index.js, }, rollupOptions: { // 排除主应用提供的公共依赖 external: [react, react-dom, antd, moment], output: { globals: { react: React, react-dom: ReactDOM, antd: antd, moment: moment, }, assetFileNames: index.[ext], }, }, }, }; }, // 在 HTML 中注入 publicPath 动态设置逻辑 transformIndexHtml(html) { return html.replace( /head, script // 动态设置 publicPath支持不同环境部署 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } /script/head ); }, }; }3.2 SVG 图标组件的兼容原 Webpack 配置中使用svgr/webpack将 SVG 文件作为 React 组件导入。Vite 中对应使用vite-plugin-svgr插件确保导入方式完全一致。3.3 传统构建产物的兼容处理项目中有 3 个老旧依赖使用 AMD 格式且不提供 ESM 版本。通过编写 Vite 插件在transform阶段做代码转换将 AMD 的define包装转换为 ESM 格式。四、性能对比与优化4.1 核心指标对比指标Webpack 5Vite提升幅度开发服务器启动单应用68s2.1s97%HMR 响应延迟3.2s62ms98%生产构建单应用4.5min1.8min60%CI 构建32 应用并行14.5min4.2min71%首屏 JS 体积gzip487KB412KB15%4.2 拆包优化策略Vite/Rollup 的默认拆包策略比较保守针对大型 SaaS 应用需要手动配置拆包策略以获得更优的缓存命中率/** * 自定义拆包策略 * * 目标将第三方依赖按更新频率分层 * 最大化浏览器缓存利用率。 */ // vite.config.ts 的 build.rollupOptions.output.manualChunks manualChunks(id: string) { // 框架层React 生态更新频率最低 if (id.includes(node_modules/react) || id.includes(node_modules/react-dom) || id.includes(node_modules/react-router)) { return framework; } // UI 层Ant Design中等更新频率 if (id.includes(node_modules/antd) || id.includes(node_modules/ant-design)) { return antd; } // 图表层ECharts体积大但更新频率低 if (id.includes(node_modules/echarts) || id.includes(node_modules/zrender)) { return echarts; } // 工具层lodash/moment/dayjs更新频率较低 if (id.includes(node_modules/lodash) || id.includes(node_modules/moment) || id.includes(node_modules/dayjs)) { return utils; } // 业务公共代码体积适中与业务迭代同步更新 if (id.includes(src/shared) || id.includes(src/common)) { return common; } // 其余第三方依赖 if (id.includes(node_modules)) { return vendor; } }4.3 迁移中的意外发现迁移完成后的一次 Code Review 中发现 Webpack 时期的ts-loader配置中transpileOnly: true开启了但对应的fork-ts-checker-webpack-plugin却在某次升级中意外失效。这意味着项目在过去 4 个月中CI 没有执行完整的类型检查。切换到 Vite 后团队同时引入vite-plugin-checker确保类型检查在开发和 CI 阶段始终有效。这一问题也直接促成了 CI 流水线中增加独立的tsc --noEmit检查步骤。五、总结六个月、32 个子应用、6800 模块的迁移技术决策的核心经验渐进式迁移优先先迁移一个中等复杂度的子应用作为样板积累配置模板和踩坑经验后再推广避免全面铺开导致的风险。兼容性优先于彻底性环境变量兼容层、别名映射等过渡代码在迁移阶段是必要的。彻底废弃旧模式应该安排在迁移稳定后作为独立迭代进行。迁移是质量检查的机会在迁移过程中发现的类型检查缺失、废弃依赖等问题应当作为迁移任务的一部分一并解决。灰度发布不可省略通过特性开关Feature Flag控制新旧构建产物的下发比例先覆盖内部用户逐步扩大到外网全量是风险最低的切换方式。在 Vite 6基于 Rolldown正式稳定后还可以考虑进一步迁移到 Rolldown 获得更快的生产构建速度。这将是下一个迭代周期的话题。