做了几年前端工程化发现很多团队手里都握着一个非常典型的组合老项目跑在 webpack 上又眼馋 Vite 的开发体验于是在项目里同时出现两套构建配置——说起来有点精神分裂但这是真实存在的过渡状态。webpack 和 Vite一个老当益壮一个后起之秀它们不是简单的替代关系更多时候是一个项目不同生命周期里各取所需的选择。这篇博文就围绕这套组合展开聊聊两者共存怎么搭、差异在哪、迁移时哪些坑我一定帮你踩过希望能给正在做构建选型或迁移评估的人一点参考。这套组合能解决的问题很实际webpack 保证生产构建的稳定性和生态兼容性Vite 在开发环境把冷启动和热更新拉到毫秒级。文章适合手里有老项目想提速、或者新项目纠结选哪套构建工具的开发者看完你至少能明确什么时候该上 Vite什么时候必须留 webpack以及怎么让它们在一套代码里和平共处。1. 为什么一个项目里会同时出现 webpack 和 Vite1.1 老项目里的 webpack 没那么容易下线大多数中大型前端项目尤其是 2018 年到 2022 年这一波积累下来的业务系统构建底座基本都是 webpack。webpack 的生态经过这么长时间沉淀几乎覆盖了所有你能想象到的构建场景老旧的 CommonJS 依赖、复杂的代码分割策略、自定义 loader 处理各种资源文件、第三方插件深度定制产物。我见过很多项目webpack.config.js 写了七八百行里面塞满了各种 loader 和 plugin 的配置有些配置甚至是从三四代同事手里传下来的根本没人敢动。这种代码库你说不用 webpack 就不用了吗不是不能是不敢。生产构建一旦出问题影响的是全部线上用户。所以团队的第一诉求往往是生产构建保持不变但开发环境能不能换个更快的1.2 Vite 的快速不光是感觉上的快Vite 给我的第一印象就是快但真正理解它为什么快是后来仔细读文档才明白的。webpack 开发服务器启动时要先做一个全量打包把所有模块从入口开始递归解析、编译、bundle 成 chunks项目越大这个启动过程越慢。而 Vite 开发环境下根本不做全量打包它直接利用浏览器原生的 ES Module 能力启动时只启动一个静态服务器浏览器请求哪个模块就实时编译哪个模块。这个机制上的差异决定了 Vite 的冷启动时间基本不受项目规模影响。一个几百个组件的中型项目webpack 冷启动可能要 20 到 40 秒Vite 通常 1 秒内就能把开发服务器拉起来。热更新更是降维打击webpack 改一个组件可能触发局部重编译Vite 则通过精确的依赖边界判断只替换真正变化的模块延迟经常在 50 毫秒以内。1.3 共存期不是妥协是工程策略很多团队在引入 Vite 时有个误区觉得要么全换要么不换。实际上构建工具的切换从来不是非黑即白。比较稳妥的做法是开发环境切 Vite生产构建继续用 webpack跑一段时间验证 Vite 环境下的依赖预构建、资源处理和开发体验都没问题了再考虑把生产构建也迁移过去。这个共存期的意义在于风险隔离。开发环境的错误最多影响开发效率生产构建的错误影响的是线上稳定性。同时双构建配置能让团队逐步熟悉 Vite 的配置方式、排查潜在依赖兼容问题等时机成熟了再做完全切换。我经手过的一个模拟项目X就是这么干的从共存到完全切 Vite 用了两个多月期间生产构建一次没出过岔子。2. webpack 和 Vite 的核心差异到底在哪2.1 模块机制打包粒度完全不同webpack 的核心模型是静态分析加打包。它会从 entry 出发构建完整的模块依赖图把业务代码拆成 chunk 后合并输出成一个或一组 bundle 文件。这个模型的好处是所有浏览器场景都兼容代价是开发环境也要经历完整的构建流程。Vite 在开发环境用的是另一套逻辑——原生 ESM。浏览器支持的script typemodule让 Vite 可以直接把未经打包的源代码发给浏览器浏览器自己解析 import 关系。Vite 只需要做一层轻量的转换比如把 TS 转成 JS、处理 JSX、解析 SFC 文件然后由浏览器完成依赖图的加载。这就是它快的最本质原因。不过这里有个关键点Vite 开发环境的快是建立在现代浏览器对 ESM 广泛支持的基础上的。如果你的目标用户还在大量使用老旧浏览器那生产构建还是得走打包路线这也是 Vite 生产环境用 Rollup 而不是直接靠浏览器的原因。2.2 依赖预构建与缓存策略Vite 在依赖处理上有一个很有特色的设计——预构建。它会在启动后扫描 package.json 里声明的 dependencies用 esbuild 把它们预先打包成 ESM 格式然后缓存到 node_modules/.vite 目录里。这个预构建解决了两件事。第一很多 npm 包发布的是 CommonJS 格式浏览器直接 import 会报错预构建把 CJS 转换成 ESM第二有些依赖内部拆成了大量小模块比如 lodash-es 这种如果不做合并浏览器启动时可能一下子发出几百个请求预构建把分散模块合并成少量 chunk减少了请求数量。webpack 没有这个机制它靠的是更细粒度的 module 缓存和持久化缓存。webpack 5 的 persistent cache 确实能大幅提升二次构建速度但首次冷启动仍然绕不开全量构建。相比之下Vite 在开发场景的缓存策略天然更轻量这让它在新项目里几乎成了标配。2.3 开发体验、生产构建与生态的三角博弈开发体验上 Vite 全面占优这个没什么争议。启动速度、热更新、终端输出甚至连配置文件都简洁到极致——一个空的 vite.config.js 就能跑起来写个 alias 和 proxy 也就那么几行。真正拉开差距的是生产构建生态。webpack 的插件体系成熟到什么程度几乎所有主流框架、工具、代码质量集成都优先支持 webpack。代码压缩、静态资源优化、Service Worker 注入、子应用拆分这些在 webpack 生态里都能找到成熟的插件。Vite 生产构建基于 Rollup插件机制也越来越完善但遇到特别冷门的需求时可能还是要自己写插件或者绕路。兼容性维度也要重点考虑。Vite 对 Node 版本有明确要求当前主版本基本要求 Node 18 以上老项目如果还在 Node 14 上跑升级成本就要算进决策里。webpack 4 用 Node 10 都能跑webpack 5 对版本也宽松很多。所以我的建议是新项目直接用 Vite 没问题老项目先确认运行环境和依赖兼容性再动手。维度webpackVite开发启动方式全量打包后启动服务器利用原生 ESM 按需编译冷启动速度10~60 秒随项目规模劣化1~2 秒基本恒定热更新体验HMR 可用大项目有卡顿精确到模块级毫秒级响应生产构建webpack 打包稳定性强Rollup esbuild产物更现代配置复杂度高需理解 loader/plugin/chunk低约定优于配置插件生态成熟稳定、覆盖广快速增长中存在少量盲区Node 版本要求webpack5 需 Node 12要求 Node 18相对严格适用场景复杂老项目、深度定制构建新项目、追求开发效率的团队3. 让 webpack 和 Vite 在同一项目里共存的实操方案3.1 双构建配置的总体设计思路先说清楚共存方案的目标开发阶段用 Vite 获得最佳体验生产构建保持 webpack 输出稳定产物同时两者共用一套源代码、一套文件结构、尽量少的条件分支。整体思路是在项目根目录同时维护 webpack.config.js 和 vite.config.js通过 package.json 的 scripts 区分启动命令。开发启动默认走 Vite需要验证 webpack 构建链路时再手动跑 webpack 命令。生产环境 CI 里先继续跑 webpack build等团队对 Vite 产物有了足够信心再切换。关键点是两个配置文件要尽量对齐四个纬度路径别名、环境变量前缀、代理规则、静态资源处理方式。这两套配置差异越大运行时出问题的概率越高。3.2 配置实例Vite 和 webpack 如何对齐直接看配置。假设项目是 Vue 3 Vite 的常规结构同时保留 webpack 构建入口。package.json 的 scripts 这样设计{ scripts: { dev: vite, dev:webpack: webpack serve --mode development, build: vite build, build:webpack: webpack --mode production } }vite.config.js 核心配置import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }, build: { outDir: dist, sourcemap: false } })webpack.config.js 保持原有配置不动但至少要让 alias 和 proxy 同步更新。如果之前没有用 alias现在正好趁这个时机统一加上或者反过来先让 webpack 支持,再让 Vite 对齐const path require(path) module.exports { entry: ./src/main.js, output: { path: path.resolve(__dirname, dist), filename: js/[name].[contenthash:8].js }, resolve: { alias: { : path.resolve(__dirname, src) } }, devServer: { port: 5174, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } }注意这里我把 Vite 默认端口设为 5173、webpack devServer 设为 5174就是防止两个开发服务器同时跑的时候端口冲突。还有一个小坑Vite 会默认把 index.html 放在项目根目录而老 webpack 项目经常把模板放在 public 或根目录下。如果你发现 Vite 起来后找不到入口先检查 index.html 的位置这是双配置阶段最常见的路径错位问题。3.3 静态资源与 public 目录的差异处理webpack 项目里public 目录下的文件会原样复制到输出目录代码里用绝对路径引用比如 /images/logo.png。Vite 处理方式类似但它多了base配置默认是/如果你的静态资源部署在子路径下Vite 产物里的绝对路径会出问题。共享方案里我的做法是源码中的资源引用尽量改成相对路径或通过 import 引入交给构建工具处理。public 里的大文件尽量不动但要在 Vite 的 base 配置里显式指定部署前缀避免打包后资源 404。这里给你的参考值// vite.config.js export default defineConfig({ base: process.env.NODE_ENV production ? /your-base/ : / })webpack 配置同样用publicPath对齐// webpack.config.js module.exports { output: { publicPath: process.env.NODE_ENV production ? /your-base/ : / } }这段配置看起来简单其实解决的是线上资源路径前缀的一致性。很多项目静态资源出问题根因就是 webpack 和 Vite 的前缀设置没对齐。3.4 环境变量与兼容层处理webpack 项目常用process.env.NODE_ENV和自定义环境变量。Vite 默认不支持process.env它用的是import.meta.env且只有VITE_开头的变量才会暴露给客户端代码。让共存阶段少改代码我给两个折中方案。第一个方案在 Vite 配置里用define做一层兼容映射例如// vite.config.js export default defineConfig({ define: { process.env.NODE_ENV: JSON.stringify(process.env.NODE_ENV || development) } })这样做之后代码里原有的process.env.NODE_ENV在 Vite 环境就不会报错了但注意这只能覆盖少量常用变量。第二个更彻底的做法写一个通用的 env 处理模块它做两件事——检测当前是 webpack 还是 Vite 环境判断import.meta.env是否存在然后从对应的全局对象里读取变量统一导出给业务层。这相当于给双构建环境加了一个适配层但改造量小得多只需要替换引用入口。如果项目里用了大量process.env.XXX形式的自定义变量我建议直接逐步迁移到统一 env 模块别在 define 里死磕。define 方案适合临时过渡长期维护起来会非常痛苦。4. 从 webpack 到 Vite 的迁移实战路径4.1 迁移前必须完成的清单检查表如果真的准备从 webpack 完全切到 Vite可以先做一套清单检查把迁移风险往前提一步。第一步是跑一份环境健康检查。确认 Node 版本 18npm 或 pnpm 的 registry 能正常访问项目没有使用太老的 Babel 配置Vite 默认用 esbuild 做 TS/JS 转换一些 Babel 插件不会自动生效。第二步是梳理构建依赖。找出 package.json 里所有 devDependencies尤其是 loaderbabel-loader、ts-loader、vue-loader 这类和 webpack plugin逐一确认是否有对应 Vite 替代方案。主流框架都有官方 Vite 插件比如 Vue 的 vitejs/plugin-vue、React 的 vitejs/plugin-react但像一些老旧的代码混淆插件、自定义 SW 插件可能要多花时间处理。第三步是小范围验证。把项目复制一个分支尝试用 Vite 把开发服务器跑起来记录报错和缺失插件再评估改动量。4.2 按模块拆解的迁移步骤实录以我迁移过一个 Vue 2 老项目为例模拟项目X从 webpack 切 Vite 的过程大致步骤是这样的先把 index.html 从 public 移到项目根目录并把入口 script 改成 ESM 方式引用。webpack 时代我们习惯写script src/src/main.jsVite 要求改成script typemodule src/src/main.js。这一步做完Vite 服务器基本上就能启动页面了剩下的都是报错修复。然后转移别名配置。把 webpack 的 resolve.alias 同步到 vite.config.js。注意 webpack 里 alias 经常用绝对路径Vite 里可以用 fileURLToPath URL 构造更稳。接着处理静态资源和 CSS。Vite 里图片、字体这类资源默认走 assetsInlineLimit 逻辑默认 4KB 以下转 base64直接引用没问题。但 CSS 里通过~引入的 node_modules 路径Vite 支持的写法可能不同一般改成import相对路径或直接 npm 包名。这一步会报错比较多耐心逐个改。然后替换环境变量。把项目里所有process.env引用找出来按前缀规则改造成import.meta.env或者统一到 env 模块。批量全局替换之前一定要先看有没有${}模板字符串拼接的场景。最后验证生产构建。Vite build 输出到 dist 目录后和 webpack 的产物对比文件名 hash 规则、资源引用路径、gzip 压缩率、chunk 数量。如果发现 chunks 切割策略差异大可以在 vite.config.js 里用 rollupOptions 手动调整。4.3 迁移后的性能与产物对比参考从实际迁移的数据看收益相当直观。模拟项目X 规模在 200 路由组件、500 依赖包左右webpack 开发冷启动约 35 秒Vite 冷启动稳定在 1.2 秒左右差距接近 30 倍。热更新从原来改一个组件等 2~3 秒降到几乎感受不到延迟。生产构建产物的对比上两者差异没有那么夸张webpack 构建时长约 75 秒Vite 约 38 秒提升了一半产物总体积 Vite 略小因为 Rollup 的 tree shaking 效果在某些场景下更彻底。但 gzip 后两者差距进一步缩小基本在一个水平。这组参考数据说明一个事实Vite 的开发体验优势远大于产物优化优势。如果你的目标是提速开发调试迁移 Vite 收益极高如果目标是让线上资源再瘦身几个百分点Vite 的收益需要结合具体项目去验证。4.4 哪些项目不建议强行迁移不是所有项目都适合迁到 Vite。我遇到几种情况会直接劝退。依赖非常古老的项目。如果项目里还有大量只能通过 webpack loader 处理的私有格式文件或者某些依赖年久失修、只支持 CJS 且无法预构建Vite 环境可能频繁报错修复成本高于收益。构建链路过深定制的项目。比如在 webpack 里通过自定义 loader 把内部文档系统的 md 文件批量转换成路由配置这类逻辑迁移到 Vite 需要重写插件。能写出来但要评估是否值得。目标环境老旧的项目。如果用户群体还大量集中在使用低版本浏览器的场景Vite 的生产产物虽然可以降级但开发环境的 ESM 功能可能无法在目标浏览器中调试会产生测试盲区。结论是迁移可以分成开发和生产两步走但不是每个项目都必须完成第二步。双构建共存本身就是一种成熟方案没必要为了“先进”而牺牲稳定性。5. 常见问题与排查实录5.1 开发环境能跑生产构建产物 404这是我在 Vite 配置里遇到频率最高的问题。现象很典型vite dev一切正常vite build后部署到服务器发现页面白屏或者 JS/CSS 资源 404。原因通常是 base 配置不对。Vite 默认的 base 是/如果你的站点部署在域名子路径比如https://example.com/my-app就必须把 base 设为/my-app/。还有一个隐蔽场景本地预览产物时用file://协议打开也会造成资源加载失败正确做法是用vite preview或起本地静态服务器来预览。排查路径打开浏览器控制台看报错的资源路径前端加上 base 前缀后去对比产物 index.html 里的引用路径就能定位到底缺在哪一层。5.2 预构建缓存导致的迷之报错Vite 预构建产物缓存在node_modules/.vite依赖更新或者切换分支后这个缓存有时会失效但不完全重建导致报错说找不到某个模块但 node_modules 里确实装了。解决办法很简单删除缓存目录后重启或者跑vite --force强制重新预构建。常见的触发场景是 git 切换分支后依赖版本变化、手动改了 package.json、以及 npm 安装中断导致缓存写了一半。遇到这个报错别急着去翻 node_modules先跑一遍vite --force大概率能解决 80% 的问题。如果强制重建后依然报错再去排查具体依赖的版本兼容性。5.3 CommonJS 依赖在浏览器端报错老项目往往依赖里混着大量 CJS 模块Vite 预构建虽然能处理大多数但有一些动态 require、条件 require、以及运行时才拼接模块路径的库预构建阶段无法静态分析。这类问题的典型报错是require is not defined或者某个模块无法解析。排查方法看报错文件是否来自 node_modules如果是在 vite.config.js 的optimizeDeps.include里显式加入这个包强制 Vite 在预构建时重点处理它。如果 include 还不够就需要换依赖或找 ESM 版本的替代包。我处理过一个案例某个图表库只提供 CJS 版本加了 include 后运行正常但打包后 chunk 体积异常变大。排查后发现是预构建把这个库打进了共享 chunk我通过vite-plugin-commonjs这类插件做额外转换才解决。5.4 webpack 与 Vite 双构建时别名不一致引发的问题共存方案的隐患往往不在单条链路上而在两套配置的对齐度。最常见的场景webpack 里 alias 的 key 是值指向src目录Vite 里配置漏了或者指向了不同路径。开发环境用 Vite 时正常CI 里切回npm run build:webpack时却报一堆模块找不到错误。预防手段很朴素把 alias 和 proxy 这类公共配置抽成共享的 JSON 配置文件webpack 和 Vite 的配置都从这个文件读取。这样两套配置永远只会有一份真相从机制上避免了配置漂移。5.5 动态 import 与 code splitting 差异webpack 里可以用魔法注释控制 chunk 命名比如import(/* webpackChunkName: xxx */ ./xxx.vue)。Vite 对动态 import 的处理方式不同注释采用 Rollup 的格式比如import(./xxx.vue)配合 rollupOptions 的 output.manualChunks 来控制分包。在共存阶段如果既写 webpack 魔法注释又写 Vite 注释最好以 Vite 为准webpack 侧能兼容就兼容不能兼容就保留一份远程配置切换。这块不用做强对齐因为两者的依赖图切分完全不同硬对齐反而增加维护成本。6. 个人实践里的最终体会从我手里过过的项目来看webpack 和 Vite 的组合更像是一把尺子的两头老项目握着 webpack 那头代表稳定和兼容新项目握着 Vite 那头代表效率和现代。真正值得纠结的不是哪个更好而是你的项目处在哪个生命周期阶段、团队能承受多大的构建重构风险。我现在遇到新项目默认直接上 Vite遇到维护中的老项目优先做开发环境的 Vite 切换保留 webpack 生产构建做风险兜底。等到依赖逐渐替换、团队对 Vite 越来越熟悉之后再评估是否把生产构建也切过去。共存不是终点但它一定是最平滑的中间态。还有一个细节值得分享无论是双构建还是完整迁移都建议把构建配置文件纳入 code review 的必查范围。构建链路的修改影响面远大于业务代码我见过团队因为调了一个 chunk 切分参数导致线上缓存失效全部资源重新下发的案例。构建配置的每一次变更都要有明确目的和验证依据这比用什么工具更重要。