
1. 为什么拿博客平台来比 Vite 和 Webpack1.1 博客场景恰好覆盖了构建器的全部核心能力我最近要重做自己的技术博客正好面临一个绕不开的选择用 Vite 还是 Webpack。单纯看官方文档、翻对比文章说实话很难有体感因为两边都把自己包装得很漂亮。我的方式很直接——同一套博客代码用两个构建器分别跑一遍从初始化、写页面、接三方库、打包优化到部署全程记录差异。这一轮实验跑下来很多东西比我看十篇对比文章都清楚。为什么选博客当测试对象因为博客这个场景太典型了。它有路由首页、文章列表、文章详情、归档有大量 Markdown 内容需要解析渲染有代码高亮这种依赖三方库的功能有多语言或标签页之类的动态路由需求还涉及静态图片、字体等资源处理。这些需求几乎覆盖了构建器日常要处理的所有问题源码编译、依赖打包、代码分割、静态资源处理、加载器定制、生产环境压缩优化。可以说把博客跑顺了一般业务项目也就八九不离十。另外博客类项目通常对部署环境要求不高静态文件往 Nginx 一丢就能跑这也方便我直接对比两者产物的实际兼容性和部署配置。即便你不是做博客的这套对比思路也适用——你完全可以用自己的业务代码复刻一次实验。1.2 我的实验环境和目标设定实验环境先交代清楚方便你复现的时候对照。Node.js 20.11.0包管理器用 pnpm 9源码同一套Vue 3.4 TypeScript 5.4 Vue Router 4同一个博客 demo首页、文章列表页、文章详情页Markdown 渲染、代码高亮、Element Plus 的部分组件按需引用Vite 分支用 5.4.xWebpack 分支用 5.9x.x vue-loader 17操作系统 macOSCPU 是 Apple Silicon这个影响构建速度的绝对数值但你可以只关注相对差距实验目标不是分出谁好谁坏而是搞清楚三个问题同一份代码在两个构建器里的配置差异到底有多大博客这种典型内容站在生产构建时该怎么针对性地做拆包和压缩日常开发时的冷启动、热更新体验差距有多大。搞明白这些以后你再给项目做技术选型心里就有一杆秤了。2. 用 Vite 搭博客初始化命令和几个关键配置2.1 创建指定版本项目别被默认模板坑到Vite 创建一个 Vue3 TS 项目网上大部分教程写的是npm create vitelatest my-blog -- --template vue-ts这条命令默认拉的是当前最新的 Vite 版本。如果你正在做一个长期维护的项目或者团队里其他人用的版本和你不一样我建议创建时直接锁版本# 创建时指定 Vite 大版本 npm create vite5.4.0 my-blog -- --template vue-ts这里有个细节create-vite的版本和生成的 Vite 版本并不是严格对应的。如果你对版本有硬要求更稳妥的做法是创建标准模板后手动安装指定版本npm create vitelatest my-blog -- --template vue-ts cd my-blog npm install npm install vite5.4.8 -D然后跑npx vite --version确认。为什么要把版本单独拎出来说因为我见过很多人项目跑不起来查到最后是create-vitelatest拉到了 Vite 6然后网上搜到的配置是 Vite 5 的写法新旧配置项对不上白白浪费时间。另外Vite 6 之后的依赖打包器逐渐切换到了 Rolldown 体系rolldown-vite配置层面虽然有兼容层但部分插件生态还在适配期博客这种依赖大量自定义插件的项目选 Vite 5 系更稳。不是让你永远停留在老版本而是做项目首先要保证可复现、可预期。2.2 博客高频依赖接入marked、highlight.js 和图标自动注册博客项目离不开 Markdown 解析和代码高亮。我用的是marked加highlight.js前者负责把.md文件转成 HTML后者负责代码块高亮。在 Vite 里接这两个库很舒服因为 Vite 支持?raw后缀直接导入文件内容// Vite 下 Markdown 文件直接以字符串导入 import article from ./posts/vite-vs-webpack.md?raw import { marked } from marked import hljs from highlight.js marked.setOptions({ highlight(code, lang) { if (lang hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value } return hljs.highlightAuto(code).value } }) const html marked.parse(article)这个?raw是 Vite 内置的不用装任何 loader非常省事。同样的需求如果放 Webpack 里你得装raw-loader或者自己写一个 loader差别一下就出来了。另一个常见需求是 UI 组件库的按需引入。我用 Element Plus配合unplugin-auto-import和unplugin-vue-components实现自动注册// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })Element Plus 的图标如果也用自动注册需要额外加IconsResolverimport Icons from unplugin-icons/vite import IconsResolver from unplugin-icons/resolver Components({ resolvers: [ IconsResolver({ prefix: icon, alias: { park: icon-park } }) ] })这个配置跑通之后你直接在模板里写el-button和icon-edit /就能用不用手动import。这套插件机制 Vite 和 Webpack 都支持但在 Vite 里配置更简单因为这些插件本身是针对 Vite 优先设计的。跑完npm run dev效果是哪怕整个项目有好几十篇文章冷启动也就一秒钟出头。你自己搭一次就知道Vite 的开发服务器根本没有整个项目编译的概念按需编译带来的爽感是非常直观的。3. 切到 Webpack同一套代码配置文件差距有多大3.1 从零手写 webpack.config.js一个简洁但完整的结构把同一套博客源码切到 Webpack这是我感触最深的一步。Vite 的vite.config.ts只有几十行而一个能跑 Vue3 TS Markdown 的webpack.config.js长这样const path require(path) const { VueLoaderPlugin } require(vue-loader) const HtmlWebpackPlugin require(html-webpack-plugin) const MiniCssExtractPlugin require(mini-css-extract-plugin) module.exports { mode: development, entry: ./src/main.ts, output: { path: path.resolve(__dirname, dist), filename: js/[name].[contenthash:8].js, assetModuleFilename: assets/[name].[contenthash:8][ext], clean: true }, resolve: { extensions: [.ts, .tsx, .js, .vue, .json], alias: { : path.resolve(__dirname, src) } }, module: { rules: [ { test: /\.vue$/, use: vue-loader }, { test: /\.ts$/, use: babel-loader, exclude: /node_modules/ }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, css-loader] }, { test: /\.md$/, use: raw-loader }, { test: /\.(png|jpe?g|gif|svg)$/i, type: asset/resource } ] }, plugins: [ new VueLoaderPlugin(), new HtmlWebpackPlugin({ template: ./index.html }), new MiniCssExtractPlugin({ filename: css/[name].[contenthash:8].css }) ] }这里还没有配webpack-dev-server、没有配tsconfig路径映射、没做splitChunks、没配环境变量注入就已经快 60 行了。注意几个关键点第一TypeScript 编译用的是babel-loader不是ts-loader。ts-loader虽然更贴近 tsc 的报错逻辑但速度明显慢而且和vue-loader搭配处理.vue文件中的script langts时需要额外的transpileOnly选项和 fork-ts-checker。博客项目规模不大用 babel 足够。第二.md文件用的raw-loader其实从 Webpack 5 开始就不是必要的了因为内置支持asset/source方式{ test: /\.md$/, type: asset/source }用这个内置方案还能少装一个依赖效果和 Vite 的?raw一样。第三CSS 抽取在开发模式其实可以不配MiniCssExtractPlugin直接用style-loader把 CSS 注入到 HTML 里时代够换了。因为开发模式追求的是热更新速度抽 CSS 反而拖慢构建。生产模式再用MiniCssExtractPlugin抽出单独文件走 CDN 缓存更合理。3.2 几个关键差异环境变量、资源加载方式与插件生态迁移过程中有几个差异是最容易踩坑的我一个个说。环境变量的获取方式。在 Vite 里写import.meta.env.VITE_API_BASE在 Webpack 里就得靠process.env.API_BASE。但现代浏览器不支持process所以必须用DefinePlugin在编译时把变量替换掉const webpack require(webpack) plugins: [ new webpack.DefinePlugin({ process.env.API_BASE: JSON.stringify(process.env.API_BASE || /api) }) ]开发模式还能借dotenv-webpack之类的插件加载.env文件但配置面一下宽了不少。Vite 是把.env支持内置了的VITE_前缀约定也省得你纠结变量命名。Markdown 文件的导入方式。在 Vite 中是路径后缀加?raw在 Webpack 中是用 rule 匹配.md后缀并交给 loader。两种思路殊途同归但 Vite 的?raw粒度更细——同一个项目里你可以既导入原始字符串又让某个路径走 plugin 转成 Vue 组件。Webpack 想做到这个粒度就得在oneOf规则里做排除麻烦不少。插件生态的适配度。unplugin-auto-import、unplugin-vue-components这一套号称双端支持但实际在 Webpack 里装过你就知道部分版本对 Webpack 5 的experiments配置有自己的要求报错信息不如 Vite 直观。另外 Webpack 生态传统上更依赖loader体系遇到处理.md变成组件这类功能最省事的做法反而是自己写一个 loader把 Markdown 编译成 Vue 组件字符串。这倒不是说 Webpack 不行而是开箱即用的程度和 Vite 不在一个量级。迁移到这一步我的结论是如果项目本来就是从零开始Webpack 的配置成本明显更高。但反过来说Webpack 的配置逻辑非常透明每个 loader、每个 plugin 都是显式声明的出了问题上手排查的思路反而清晰。这也是 Webpack 在大型复杂项目中依然有生存空间的原因。4. 生产构建优化压缩器选型、拆包策略和体积实测4.1 esbuild 和 terser 压缩出来的产物差异在哪Vite 生产构建默认用 esbuild 做压缩但它也支持换成 terser。这个选择在博客项目里直接影响了产物体积和构建速度。esbuild 压缩的特点是快快到什么程度同样是 200 多个模块的项目esbuild 压缩耗时在几百毫秒级别而 terser 需要十几秒甚至更久。如果你不想等就用 esbuild 默认方案几乎不消耗感知时间。但 esbuild 的压缩率通常比 terser 差一些对很多旧语法特性的兼容也没 terser 全面。Vite 里切到 terser 很简单export default defineConfig({ build: { minify: terser, terserOptions: { compress: { drop_console: true, drop_debugger: true } } } })drop_console这行非常实用博客上线后谁也不想用户控制台里看到一堆 console.log。不过要注意terser 模式下构建时间会明显拉长。我在博客项目里跑出来的对比数据大致是这样压缩方式产物总体积构建耗时含压缩gzip 后体积esbuild约 152 KB约 1.8 s约 47 KBterser约 146 KB约 14.6 s约 45 KB也就是说terser 帮我省了 6% 的体积但构建时间多了 8 倍。博客这种内容型站点产物体积差这几 KB 对加载速度几乎没影响我最终选了 esbuild。如果你是做后台管理系统这种对包体积敏感且发布频率低的项目terser 还是值得等的。4.2 博客项目的拆包策略vendor、Markdown 相关库和路由懒加载生产构建的另一件大事是拆包。博客项目的拆包思路和线上商城完全不同你要做的是把变化频率差不多的资源分到一起让用户二次访问时能从缓存里直接命中。我的博客拆成三大块Vue 相关的基础库、Markdown 和代码高亮相关的三方库、业务代码。Vite 里用manualChunksbuild: { rollupOptions: { output: { manualChunks: { vendor: [vue, vue-router], markdown: [marked, highlight.js] } } } }注意manualChunks接受对象写法时key 是 chunk 名value 是模块列表。rollupOptions里还可以写output.chunkFileNames控制生成的文件名格式output: { chunkFileNames: js/[name].[hash:8].js, entryFileNames: js/[name].[hash:8].js }Webpack 对应的拆包配置稍微绕一些用optimization.splitChunksoptimization: { splitChunks: { chunks: all, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/](vue|vue-router)[\\/]/, name: vendor, priority: 20 }, markdown: { test: /[\\/]node_modules[\\/](marked|highlight.js)[\\/]/, name: markdown, priority: 10 } } } }在 Webpack 里splitChunks还支持maxSize、minSize等参数用来控制拆分的粒度。比如你有个 500 KB 的依赖可以设置maxSize: 200000让它进一步拆分。但别拆太碎HTTP/2 多路复用虽然能扛住很多请求浏览器解析和执行的成本还是在的拆出几十个几百字节的碎包完全没有意义。路由懒加载也别忘。博客详情页的 Markdown 内容不小放在首屏路由里会拖慢首屏速度。改成动态导入后只有用户点进详情页才加载对应 chunkconst ArticleDetail () import(./views/ArticleDetail.vue) const routes [ { path: /article/:id, component: ArticleDetail } ]Vue Router 会自动把懒加载的路由组件拆成独立 chunk。这样首屏只需要加载首页相关代码文章详情页和 Markdown 解析库都留着后续按需拉取首屏体积能从 150 多 KB 降一半左右。5. 部署绕不开的坑base 路径、文件哈希和旧产物缓存5.1 Vite 的 base 和 Webpack 的 publicPath 是同一件事博客搭好之后要部署。这步最常见的问题是部署在子路径下比如域名是example.com/blog/那所有静态资源的引用路径都得加/blog/前缀。Vite 里的配置是base// vite.config.ts export default defineConfig({ base: /blog/ })如果你不配默认是/部署到子目录后 CSS、JS 全部 404。这个坑我踩过不止一次每次换部署平台都要重新检查。Webpack 对应的配置叫publicPathoutput: { publicPath: /blog/ }还有一个细节是如果你的博客是部署在 CDN 上的独立域名可以直接把base或publicPath设成完整的 URLbase: https://cdn.example.com/blog/但这么做有个副作用本地开发时所有资源也会指向 CDN 地址调试时每次都命中线上文件。解决办法是用环境变量区分export default defineConfig({ base: process.env.NODE_ENV production ? https://cdn.example.com/blog/ : / })5.2 我用哈希命名踩过的缓存旧资源坑为了利用浏览器缓存生产构建的文件名必须带内容哈希。Vite 默认生成[name].[hash:8].js这种格式Webpack 也可以配置[contenthash:8]。配置本身不难难的是你部署的时候容易漏掉一个点入口 HTML 文件本身不能设长期缓存。我有一阵子把index.html也放到了 CDN 上缓存结果每次发版后用户拿到的还是老 HTML里面的 JS 文件名引用还是旧的新内容根本不会加载。排查了半天才发现是 CDN 缓存层级的问题。正确做法是index.html用no-cache让浏览器每次回源确认静态资源文件带哈希可以放心设max-age31536000。如果你用 Nginx可以参考这个配置location /blog/ { add_header Cache-Control no-cache; try_files $uri $uri/ /blog/index.html; } location /blog/assets/ { add_header Cache-Control public, max-age31536000, immutable; }第一个 location 处理 HTML第二个处理带哈希的静态资源。这样发版时旧资源会被新哈希文件替代index.html每次都能拿到最新的引用关系。这套思路对 Vite 和 Webpack 构建的产物都适用算是内容型站点的通用部署经验。5.3 旧 chunk 文件清理不彻底的一个小教训有一次发布后我在 Nginx 的目录里发现堆积了很多旧版本的 JS 文件都是之前发版留下的。Vite 构建时outDir下面的旧文件通常会被emptyOutDir自动清掉Webpack 也有output.clean: true。但如果你的构建流程是先生成到临时目录再手动同步到服务器就很容易把几十个旧 JS 一起同步上去。我现在的做法是构建和部署之间加一步校验部署脚本里先对比本地 dist 目录与线上目录的哈希文件列表只同步新增和变化的部分同时在服务器保留最近 2 个版本的备份万一新包有问题能快速回滚。这个策略对博客够了再大一点的项目就上对象存储加 CDN 刷新的方案。6. 开发体验实测冷启动、热更新和依赖预构建6.1 两套构建器的冷启动和 HMR 差距博客项目规模不大但开发体验的差距仍然非常直观。Vite 在开发模式下利用原生 ESM浏览器直接通过script typemodule加载模块开发服务器只做按需转译冷启动时间在我的项目里是 0.9 秒左右。改一个组件文件HMR 响应基本是毫秒级几乎感知不到重刷的过程。Webpack 的 dev server 启动时需要把整个项目从入口开始构建一遍模块依赖图我的这个迷你博客项目也要 6 到 8 秒。改一个组件后Webpack 的 HMR 通常能控制在 1 秒内但相比 Vite 还是能感觉到顿挫。这个差距在项目变大、模块变多之后会更明显。Vite 的按需编译意味着不管项目多大冷启动都很快Webpack 的编译时间则随着项目规模线性增长。组里一个老项目有三百多个页面Webpack dev server 冷启动要四十多秒换 Vite 后降到两秒多开发体验完全是两个时代的东西。所以只要条件允许新项目直接上 Vite 是基本共识了。6.2 依赖预构建Vite 的加速利器也是偶尔的坑Vite 开发模式下有一个【依赖预构建】机制用 esbuild 把node_modules里的依赖预先打包成 ES module 格式。这样做有两个目的第一很多第三方库直接以 ESM 形式引到浏览器会因为兼容问题报错第二大量小模块预打包成一个文件能显著减少浏览器里发起模块请求的数量。但正因为有预构建你在使用中可能遇到一个经典报错[vite:esbuild-transpile] transform failed with 2 errors这个报错通常是依赖里的某个文件语法和老旧的 esbuild 不兼容或者 node_modules 里某个包在预构建时解析失败。解决办法一般有几种// 方式一把报错的包加入 optimizeDeps.exclude跳过预构建 export default defineConfig({ optimizeDeps: { exclude: [some-package] } })// 方式二删除 node_modules/.vite 缓存后重试 rm -rf node_modules/.vite// 方式三锁版本 pnpm add some-packagefixed-version大多数情况下方式二最见效。你可能看到这会觉得 Vite 有点脆弱但实际开发中这个坑出现频率并不高而且大多数依赖升级后都能解决。相比 Webpack 那种报错要翻整个依赖图的局面Vite 的排错路径反而清楚得多。6.3 Vite 6 和 Rolldown 带来的变化预期聊到 Vite现在绕不开 Vite 6 和 Rolldown。简单来说Rolldown 是字节跳动开源的用 Rust 写的打包器被 Vite 团队收编之后作为底层的下一代打包引擎。以前 Vite 开发用 esbuild、生产用 Rollup两套引擎行为不完全一致偶尔会出现开发正常但生产构建报错的情况。Rolldown 的目标是统一这两套路径。当前rolldown-vite还在迭代中生产环境大规模使用还需要观望。如果你和我一样现在要做一个稳定的项目我建议用 Vite 5 系 现有 Rollup 插件保持惯性。但值得关注 Rolldown 的进展因为一旦稳定下来Vite 的构建速度还会再上一个台阶。这个演进情况也给 Webpack 用户一个提醒Webpack 的生态虽然稳如老狗但它的底层架构决定了它的构建速度天花板就在那里。如果你维护的老项目已经因为构建速度被拖累到影响开发效率认真考虑迁移到 Vite 是值得的尤其是 Vue 技术栈的项目。7. 这一轮对比下来我的实际选型思路跑完整轮实验我自己的判断是这样的博客类、内容站、中小型业务项目直接选 Vite。开发体验好、构建快、配置简单团队的效率收益非常明显。现在 Vue 和 React 的新项目官方模板也都默认给了 Vite生态已经不是问题。大型老项目、依赖丰富且定制化程度极高的工程继续留在 Webpack。如果项目里已经沉淀了大量自定义 loader、复杂的构建脚本迁移成本会吃掉 Vite 带来的收益。尤其是用了大量 Webpack 特有的loader和plugin迁移不只是改配置文件还要重写一堆跟具体 loader 绑定的逻辑。混合策略也常见。典型做法是老项目的增量部分继续 Webpack 构建同时抽出新模块用 Vite 做独立的子应用配合微前端的方式逐步过渡。这个路子慢但稳。博客项目本身我用 Vite 跑通后产物体积优化到位、部署缓存策略也对齐了整体很满意。唯一还要留个心眼的是插件兼容性比如我需要用自定义的 Markdown 渲染流水线Vite 的插件接口和 Rollup 基本一致写起来不复杂生态里也有不少现成的 Markdown 插件可以抄作业。最后分享一个关于工具选型的个人经验不要光看框架作者说得多好也不要盲目信从XX已经过时了的说法。拿自己的真实项目做一次对比实验记录冷启动时间、HMR 响应、配置量、构建产物、部署踩坑数量这几项数据比任何文章都有说服力。毕竟工具是拿来解决实际问题的不是拿来站队的。