ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Webpack太慢?Vite迁移实战:构建优化与配置指南

2026/10/7 20:40:58 拓冰建站 浏览量
Webpack太慢?Vite迁移实战:构建优化与配置指南 1. 为什么 Webpack 开发时那么慢bundler 编译模型的瓶颈我之前接手过一个中后台项目React TypeScript页面模块大概 300 多个node_modules里的依赖也不少。刚开始用 Webpack 4 开发一次npm run dev冷启动要 28 秒左右中途改了某个公共组件热更新等 3 到 5 秒是常事。后来项目膨胀到 600 多个模块冷启动直接逼近 40 秒热更新有时候要 8 秒甚至直接白屏等刷新。这不是个案几乎所有 Webpack 项目到后期都会遇到同样的问题。问题出在 Webpack 的编译模型上。Webpack 是一个典型的 bundler它必须从入口文件开始递归地解析所有import/require构建出一整张模块依赖图module graph然后逐个模块交给 loader 转换代码再把这些模块打包成一个或几个 bundle 文件。也就是说哪怕你只想改一个按钮的颜色Webpack 也得先把整个项目的模块都读一遍、转换一遍、打包一遍浏览器才能拿到更新后的代码。项目越大这张依赖图越大耗时自然线性上升。开发模式下 Webpack 还会给每个模块包一层运行时注入代码用来实现module.hot.accept这类热更新能力。这个包装逻辑本身也有开销而且模块数量越多bundle 里附带的管理代码就越多解析和执行效率就越差。有人可能会说那 Webpack 也有持久化缓存、多线程构建啊为什么还是慢因为这些优化本质上是在缓解全量打包这个行为而不是消除它。cache-loader能把 loader 转换结果存到磁盘但依赖图还是要重新遍历thread-loader能把压缩或 babel 转换拆到子进程跑但任务本身还是得做。DLLPlugin 是一个更极端的方案把不常变的第三方依赖预编译成单独的文件但它带来的心智负担非常高配置复杂、容易踩版本坑而且 Webpack 5 官方已经在文档里明确不推荐继续使用了。所以你会发现一个很尴尬的事实Webpack 的优化手段越来越多项目的构建复杂度也越来越高但开发体验的提升却非常有限。这不是配置技巧的问题而是打包这个模型在开发场景下天然就低效。真正的突破口是换一种完全不同的运行机制——Vite 就是基于这个思路出现的。2. Vite 快在哪里ESM 原生与依赖预构建的正确理解Vite 的核心思路其实不复杂开发阶段不打包。浏览器已经原生支持 ES Module通过script typemodule可以直接加载远程 JS 模块不需要像 Webpack 那样把代码全部转换为非模块化的 bundle。Vite 利用这一点在开发服务器收到页面的 HTML 请求后只做两件事把入口模块的源码转译一下返回给浏览器然后在浏览器真正import某个模块时才去按需转译那个模块。它没有构建依赖图这一步也没有生成 bundle这一步冷启动自然就是秒级。Vite 开发模式下的两种核心机制值得好好讲清楚它们决定了为什么它能比 Webpack 快出数量级。2.1 依赖预构建把 CJS 模块变成浏览器能认的 ESMnode_modules里有大量第三方库是基于 CommonJS 或 UMD 格式写的浏览器根本不认识require。如果 Vite 不做任何处理浏览器在加载这些依赖时直接就会报错。所以 Vite 在首次启动时会对node_modules做一次依赖预构建用 esbuild 把这些依赖统一转换成 ESM 格式并把多次import同一个包的内部引用做重写合并同时把结果缓存到node_modules/.vite目录。这个预构建过程非常快因为 esbuild 是用 Go 写的打包 JS 代码的速度比传统 JS 写的打包器快几十倍甚至上百倍官方说法是在 Terser 等工具的对比测试中能拉开好几十倍差距。实际体感也很明显第一次启动 Vite预构建一个包含 100 多个第三方依赖的项目通常一两秒内就结束了。而 Webpack 首次构建光跑 babel 转译就得十几秒起步。还有一点很多人没注意到esbuild 预构建完成后Vite 会把依赖的缓存关系记在optimizeDeps的哈希里。如果你没有改package.json后续启动直接用缓存连预构建这步都省了启动速度还能再快一截。2.2 源码按需转译只有被请求的模块才被处理预构建处理完依赖后Vite 的 dev server 会返回一个入口 HTML里面只有一个script typemodule src/src/main.ts。浏览器加载这个入口后会顺着源码里的import语句一个个发请求给 Vite dev server。Vite 收到请求后对当前这个文件单独做转译——比如把.ts转成.js、把.vue拆成 JS 和 CSS、把 SCSS 编译成普通 CSS——然后立刻返回。这个模型的关键在于转译是惰性的。编辑某个模块Vite 只需要重新转译那一个文件然后通过 HMR 连接把更新的模块推给浏览器。浏览器端只需要重新请求那一个模块不需要重新加载整个应用。所以修改样式或者改一个工具函数浏览器里的反馈几乎就是即时的不像 Webpack 那样要等整个依赖图重新走一遍。我之前在项目里实测过Vite 的热更新响应时间基本稳定在 50ms 左右Webpack 则是 1.5 秒起步快的时候也得 800ms。2.3 对比实测同样项目切换前后的体感差异我拿那个 600 模块的 React 项目做了一次完整迁移切到 Vite 之后用hyperfine做了冷启动对比数据比较直观指标Webpack 4优化后Vite 4冷启动 dev server约 38 秒约 1.2 秒首次页面加载完成约 42 秒约 2 秒修改一个组件的热更新2~8 秒约 80ms全量构建约 120 秒约 60 秒开发体验完全是两个时代的产物。生产构建虽然也快了一倍但真正的质变还是开发阶段的这几十倍差距这也是很多人切换后最大的体感来源。不过话说回来开发模式快是一回事生产构建要稳定、产物要小是另一回事这正是下一章要展开的内容。3. 迁移实操webpack.config.js 到 vite.config.ts 的配置对照很多人一听迁移就头大其实 Vite 的设计很讨巧它很懂 Webpack 用户的习惯大量配置项的命名和结构都沿用了社区熟悉的概念。把项目从 Webpack 切到 Vite多数情况下不需要重写业务代码主要工作量集中在配置文件和环境差异的处理上。下面是几个最常见的配置项对照基本上照着改就能跑通。3.1 入口、出口与基础路径Webpack 里入口和出口是最核心的配置Vite 的入口是隐式的——默认读取项目根目录下的index.html然后从里面的script typemodule src/src/main.ts找到入口 JS。如果你不需要多页面基本不用显式写入口配置。出口就更不用管了Vite 会把最终产物输出到dist目录。基础路径这个要特别注意。Webpack 对应的是output.publicPathVite 对应的是base配置项// webpack.config.js module.exports { output: { publicPath: process.env.NODE_ENV production ? /admin/ : / } };// vite.config.ts import { defineConfig } from vite; export default defineConfig({ base: process.env.NODE_ENV production ? /admin/ : / });如果你把应用部署在 CDN 或子路径下这个配置错了页面会直接白屏因为 JS 和 CSS 的引用地址全都会错。这是迁移时最容易出问题的地方之一我见过不止一个项目栽在这里。3.2 别名aliasresolve.alias几乎是每个项目都有的配置Vite 的写法和 Webpack 几乎一模一样只是要注意用 Node 的path来解析路径否则相对路径会算错// webpack.config.js const path require(path); module.exports { resolve: { alias: { : path.resolve(__dirname, src) } } };// vite.config.ts import path from path; import { defineConfig } from vite; export default defineConfig({ resolve: { alias: { : path.resolve(__dirname, src) } } });有一点和 Webpack 不同的是Vite 对alias的匹配默认是前缀匹配会匹配到/components也可以匹配到vue这种包名。如果你用的自定义别名和某个 npm 包名冲突了可能会出现意料之外的解析结果。稳妥做法是给别名加上精确边界Vite 内部推荐写成: path.resolve(__dirname, src)并且配合find和replacement对象写法来控制匹配精度具体可以查一下文档里的resolve.alias用法。3.3 代理与 MockWebpack 的devServer.proxy在 Vite 里对应server.proxy配置结构几乎完全一致// webpack.config.js devServer: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }// vite.config.ts server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }Vite 底层用的是http-proxy所以changeOrigin、rewrite这些选项都是兼容的。唯一需要留意的是Webpack 的devServer.before这类自定义中间件能力Vite 里需要用 Vite 插件的configureServer钩子来实现写法更接近中间件模式但思路是通的。3.4 CSS 与 PostCSS 处理Webpack 里处理 CSS 通常要配style-loader/css-loader/postcss-loader一长串 loader。Vite 内置了 CSS 处理能力默认支持 CSS 文件、.scss、.less、.stylus只需要安装对应的预编译器比如sass然后直接import就能用。PostCSS 也内置了项目根目录如果有postcss.config.jsVite 会自动读取并使用。这个设计大大降低了配置成本但也带来一个很实在的问题Vite 默认不开启 CSS 的url()路径重写方式。Webpack 中你会习惯用~前缀去引用node_modules里的样式文件比如import ~bootstrap/dist/css/bootstrap.min.css。在 Vite 中不需要这个~前缀直接写import bootstrap/dist/css/bootstrap.min.css就能解析到 node_modules如果你保留老的写法反而会报错找不到模块。3.5 环境变量Webpack 常用dotenv-webpack或自定义DefinePlugin来注入环境变量。Vite 内置了环境变量机制规则是只有以VITE_开头的变量才会被注入到客户端代码中从import.meta.env上读取// 直接在代码里用 const apiBase import.meta.env.VITE_API_BASE;// webpack 时代也是可以用的 const apiBase process.env.VITE_API_BASE;如果你有一批老代码里写的是process.env.VITE_API_BASE可以统一在 Vite 的define里做一个全局替换export default defineConfig({ define: { process.env: { VITE_API_BASE: JSON.stringify(process.env.VITE_API_BASE) } } });注意要JSON.stringify包一层否则替换进去的是一个裸字符串代码里会变成const apiBase http://xxx直接语法报错。3.6 plugins 机制差异Webpack 的插件体系基于 tapable 钩子Vite 的插件则是标准的 Rollup 插件接口外加 Vite 特有的一系列钩子config、configureServer、transformIndexHtml等。业务上最常见的需求是 Vue 项目加vitejs/plugin-vue、React 项目加vitejs/plugin-react这些官方插件装好就行不需要额外做 Babel 配置。如果你原来在 Webpack 里写的是一个复杂自定义插件迁移到 Vite 基本都要重写。所以迁移前评估一下自己项目里自定义插件的复杂程度是判断迁移成本最直接的办法。通用能力类的插件压缩、图片优化、CDN 外链大多都有 Vite 移植版但非常特定于你团队内部那套构建逻辑的插件是最费时间的部分。4. 生产构建的另一套逻辑Rollup 打包与产物优化Vite 开发模式快靠的是不打包但生产环境是要真正产出静态文件的总不可能也让用户浏览器一个个请求几千个模块。所以 Vite 在vite build时会把代码交给 Rollup 来做全量打包和压缩。很多人不理解为什么 Vite 不继续用 esbuild 做生产构建一个很重要的原因是esbuild 的代码分割code splitting能力还不够完善而 Rollup 的 tree-shaking 和 chunk 分割机制非常成熟产物体积控制得更好。Vite 官方也表态会在未来逐步引入 Rolldown基于 Rust 写的 Rollup 替代品但现阶段 Rollup 仍然是最稳的选择。生产构建阶段最值得关注的是三个问题分包策略、CDN 外链和浏览器兼容性。4.1 手动分包避免只改业务代码就拖着依赖重新打包依赖和业务代码的分离在生产构建里非常重要。如果不做分包第三方库会跟着整个应用打成一个巨大的 JS 文件用户每次发布新版本都要重新下载几 MB 的依赖代码缓存策略形同虚设。Webpack 里用的是optimization.splitChunksVite 里对应的是build.rollupOptions.output.manualChunks。比如把 Vue 全家桶拆成一个 vendor 包export default defineConfig({ build: { rollupOptions: { output: { manualChunks(id) { if (id.includes(node_modules)) { if (id.includes(vue) || id.includes(vue-router) || id.includes(pinia)) { return vue-vendor; } if (id.includes(echarts) || id.includes(zrender)) { return echarts-vendor; } return vendor; } } } } } });这里有个常见的坑manualChunks的函数形式里过滤条件写得过宽很容易把存在循环依赖的包拆到不同 chunk 里运行时报Cannot access before initialization之类的初始化顺序错误。稳妥做法是只对明确没有交叉依赖的库做强制分包剩下的让 Rollup 自动处理。4.2 通过 external 把依赖甩给 CDN另一个常用优化是external加 CDN生产环境不打包 jQuery、lodash 这类体积大且更新不频繁的库而是直接在index.html里引 CDN 地址。Vite 配置如下export default defineConfig({ build: { rollupOptions: { external: [lodash, jquery], output: { globals: { lodash: _, jquery: $ } } } } });同时要在index.html里手动加上 CDN 的script标签。这里很容易漏一步代码里import _ from lodash在运行时拿的是全局变量_如果 CDN 挂了或者加载顺序不对页面就会白屏报错。所以用 CDN 外链一定要确认第三方服务的可用性并且做好本地 fallback。4.3 老浏览器兼容plugin-legacy 还是自动降级Vite 5 的默认构建目标基线是 ES2020如果你要兼容 IE 11 或者低版本移动端浏览器必须加vitejs/plugin-legacy。这个插件做的事情是生成一份现代代码给新浏览器用同时生成一份降级转译版本给老浏览器用再通过nomodule属性做差异加载。我在迁移一个运营管理系统时踩过这个坑这个系统还在用 IE 11迁移到 Vite 后我没加 legacy 插件结果 IE 里打开白屏控制台报错是语法错误——代码里用了可选链?.IE 根本不认识。加上 plugin-legacy 之后这个问题就解决了但产物体积会增加因为有两份代码。所以要不要加这个插件取决于你的目标用户群体别盲目加。5. 迁移中高频踩坑与排查思路这一节集中写写迁移过程中最容易卡住的地方都是我实际踩过的按出现频率排序。5.1 动态 import 与字符串拼接路径Webpack 里写import(./modules/${name}.js)是可以动态解析的Webpack 会把你给的所有可能的路径都打包进去运行的时候再根据变量选择。Vite 不行它不能对运行时才知道的变量路径做静态分析必须用import.meta.glob显式声明所有可能的匹配路径// 老写法Vite 会直接报错 const module await import(./modules/${name}.js); // Vite 写法 const modules import.meta.glob(./modules/*.js); const loader modules[./modules/${name}.js]; // loader 是一个懒加载函数调用后才真正 import const module await loader();如果项目里动态导入的路径模式比较复杂这一步需要花一点时间逐一改写。5.2 CJS 老依赖的预构建失败有些老旧依赖因为导出方式特殊第一次启动 Vite 时预构建会报错常见的错误特征是在浏览器控制台看到does not provide an export或者请求node_modules下某个包时返回 500。这种问题可以调整optimizeDeps配置来绕过export default defineConfig({ optimizeDeps: { include: [lib-a, lib-b], exclude: [lib-c] } });include强制把某些包纳入预构建exclude则是把有问题的包排除让 Vite 在源码引用时再单独处理。排除之后可能需要降级策略比如改用其他包或者让这个依赖不接受 HMR。多数的预构建问题其实是包版本过老导致的升级依赖往往更省心。5.3 Node 内置模块的 polyfill 消失Webpack 4 会自动给浏览器端的crypto、path、process等 Node 内置模块提供 polyfill很多老代码因此可以在浏览器里直接用path.join这类 API。Vite 不做这件事默认会直接报错提示某个模块在浏览器环境不可用。这不是 Vite 的缺陷而是浏览器本来就不该有这些 API。遇到这种代码正确的处理方式是改成浏览器原生的 Web API或者用rollup-plugin-node-polyfills这类插件按需注入。最坑的是那种依赖 Node 模块做复杂运算的库比如某些加密算法库在浏览器端跑通需要做不少适配工作。5.4 图片与静态资源的引用路径Vite 处理图片资源的逻辑是小于build.assetsInlineLimit默认 4096 字节的图片会转成 base64 直接内联进 JS大于这个阈值的会输出到dist/assets并按需生成带哈希的文件名。这个行为本身和 Webpack 的limit配置差不多但有两个差异值得注意。第一代码里通过/src/assets/logo.png引用的资源在构建时会自动处理路径但如果资源放在public目录就直接拷贝到dist根目录不会被内联。很多迁移项目的问题是之前的 Webpack 项目里用publicPath 文件名引用的资源切到 Vite 后路径对不上需要统一改成相对路径或import.meta.env.BASE_URL 路径。第二动态拼出来的 URL比如:src/img/ name .pngVite 不会替你处理因为它无法预知这些文件。这种情况下要么把图片放到public目录要么用import.meta.glob把图片导入映射做好。5.5 Vite 版本差异带来的配置陷阱这几年 Vite 的版本更新很快4.x 到 5.x 再到 6.x最明显的变化有两处一是配置项命名调整比如部分选项从build迁移到optimizeDeps二是对 Node 版本的要求越来越高。我在迁移时因为项目还在用 Node 14Vite 5 直接拒绝启动后来把本机 Node 升级到 18 才解决。建议动手前先确认开发环境和 CI 里的 Node 版本再决定用哪个大版本的 Vite不然一篇配置照着文档抄也可能跑不起来。还有一个细节vite preview命令预览的是生产构建产物不是开发服务器很多人在部署后发现资源路径不对第一反应是去改 dev 配置实际上要改的是base。6. 结合热词补充Webpack 打包优化配置的本质与新项目的选型建议搜索热词里除了 Vite 之外还有不少人在搜webpack 打包优化配置包括webpack 配置这类关键词说明 Webpack 仍然是大量存量项目的底座。如果暂时没有条件切到 Vite掌握下面这些 Webpack 优化思路也是必要的至少能缓解开发期的痛苦。6.1 存量 Webpack 项目的止血优化Webpack 5 比 Webpack 4 原生就带持久化缓存cache: { type: filesystem }启动和构建都会明显提速这是零成本收益最大的一项。开启后二次构建的耗时一般能下降 40% 到 60%。在此基础上再考虑用thread-loader给耗时的 babel-loader 开启多线程用cache-loader缓存 loader 的中间产物。注意thread-loader并不是所有场景都划算文件很小的项目启动线程池的开销反而比省下的时间还大实测下来一般 100 个模块以上的项目才有明显收益。代码分割当然是必做的。单 bundle 的体积控制是原生体验的关键优先把react/react-dom或vue这类框架级的依赖拆出来配合 CDN 外链把首屏加载量降下来这个对用户体验的改善比构建速度更直接。还有一招容易忽略在开发模式下关闭不必要的压缩插件特别是terser-webpack-plugin。生产构建要压缩开发环境完全不需要很多团队的 Webpack 配置里 compression 插件是全局生效的白白拖慢 dev server 的启动和热更新。6.2 新项目怎么选型新项目或者可以完全掌控技术栈的项目我倾向于直接用 Vite。理由不只是快而是 Vite 的配置心智负担比 Webpack 轻得多内置能力已经覆盖了大部分常见需求不需要像 Webpack 那样把 loader、plugin、optimization 全部搞清楚才能开工。团队里有新手入职Vite 的项目他能很快上手改配置Webpack 项目至少需要一周沉淀才能动构建配置这个团队成本差异很容易被低估。唯一建议谨慎考虑 Vite 的场景是项目里有大量需要深度定制构建流程的模块分包策略或者强依赖 Webpack 特有的 loader 生态比如某些小众格式文件的解析器这部分迁移成本会高一些。即便如此也可以先只把开发环境切到 Vite官方支持加vitejs/plugin-legacy用 Vite dev server 提升开发体验生产环境暂时保留 Webpack 构建分阶段过渡风险可控。7. 个人体会与最后的实操建议这次从 Webpack 迁移到 Vite最大的收获其实不只是速度变快了而是整个团队的开发节奏都变了。之前改个页面组件热更新要等好几秒心态上是改完去倒杯水再回来看效果现在热更新几乎是敲完保存就生效更愿意做小步快跑的迭代这种开发体验的提升很难用数字完全表达。给准备做迁移的读者几个具体建议第一迁移前先跑通一个最小可行性验证。挑一个模块数较多的业务页面把它单独拎出来切到 Vite 跑通确认所有依赖预构建都正常再做全量迁移。这样能把风险控制在最小范围。第二配置改完后一定要测三种模式dev、build、preview。很多配置在 dev 下正常、build 就出问题比如图片路径、环境变量、CDN 外链preview 能帮你提前发现部署后的资源引用错误别等上了服务器才发现白屏。第三公共库和业务代码的分包要提前设计。Vite 默认的产物已经比 Webpack 默认更合理但如果你有路由级懒加载需求务必在迁移时同步确认manualChunks的拆分是否符合预期避免把整个路由页面的公共依赖全打进一个 chunk导致首屏体积不降反升。最后想说一句Vite 和 Webpack 不是非此即彼的关系。Webpack 在复杂分包和生态深度上依然很能打Vite 在开发体验上是降维打击。现阶段最优解往往是让 Vite 负责日常开发同时理解 Webpack 的优化逻辑因为存量代码总有一天需要你来维护。工具会迭代但理解构建链路本质这件事永远是前端工程化里最值钱的能力。