Webpack的困境:
Webpack 启动 dev 服务器的逻辑是:先打包,再启动
当你敲下 npm run dev,Webpack 会从入口文件(如 main.js)开始,递归地扫描所有依赖,调用各种 Loader(Vue-loader, Babel-loader)进行编译,最后在内存中构建出一张完整的依赖关系图(Dependency Graph)。
只有当这张图构建完成后,Dev Server 才会显示“Compiled successfully”,你才能访问页面。
这就是痛点: 如果你的项目有几千个模块,Webpack 就要先处理完这几千个模块。这就解释了为什么大型 Webpack 项目启动时,进度条会卡在 70%~80% 很久,你只能盯着屏幕等它“打包”完
Vite的启动原理:
依赖预构建
Vite 启动的第一件事不是看你的代码,而是看你的 node_modules。
- 动作:调用 esbuild 将 vue、axios 等 CommonJS 格式的第三方库转换成原生 ES 模块,并打成一个大包。
- 目的:解决浏览器不认 require 的问题,并减少成百上千个小文件的 HTTP 请求。
- 结果:生成的文件放在 node_modules/.vite 中。
启动 HTTP 静态服务器
- 动作:瞬间启动一个基于 Node.js 的本地服务器。
- 特点:此时没有任何打包动作,服务器只是空壳,因为它不需要像 Webpack 那样先生成一个 bundle.js。
浏览器解析入口
- 动作:你访问 localhost:5173,服务器返回 index.html。
- 关键代码:浏览器看到 <script type="module" src="/src/main.ts"></script>。
- 浏览器反应:原生浏览器引擎识别出 type="module",它会主动向服务器发起一个请求:“请给我 /src/main.ts 这个文件”。
按需动态转换 (最关键)
- 动作:服务器收到 /src/main.ts 的请求,发现它是 .ts 后缀,于是立即在后台调用内部插件将其转译为 .js。
- 路径重写:你的代码里写着 import App from './App.vue'。浏览器又会发起请求:“请给我 /src/App.vue”。
- Vue 处理:Vite 此时调用 vue-loader 类似的插件,把 .vue 文件拆解成 JS 和 CSS 发给浏览器。
- 只做有用的事:如果你有 100 个组件,但首页只用了 2 个,Vite 此时只编译这 2 个,剩下的 98 个文件它碰都不会碰。
第五步:浏览器端缓存
- 动作:Vite 在返回文件时,会根据文件类型加上不同的 HTTP 缓存头。
- 逻辑:第三方依赖(预构建好的)被强缓存(以后不再请求);业务代码(你写的)被协商缓存(变了才请求)。
- 结果:第二次刷新页面,几乎所有东西都来自浏览器内存或磁盘。
第六步:极速热更新 (HMR)
- 动作:当你修改 App.vue。
- 逻辑:Vite 只需要让浏览器重新请求这 一个 文件的 URL 即可。
- 优势:由于模块之间是原生解耦的,更新速度始终是毫秒级,无论项目多大。
为什么vite预构建比webpack快那么多?
esbuild (速度的本质)
Webpack 转换代码用的是 JavaScript 编写的加载器(如 Babel),而 Vite 预构建用的是 esbuild。
- 语言优势: esbuild 是用 Go 语言 编写的,并编译成了机器码。JavaScript 是解释型语言,在处理大规模代码扫描和打包时,Go 的执行效率比 JS 快 10 到 100 倍。
- 并行处理: Go 语言天然擅长多线程并行处理,而 JS 是单线程的(虽然可以通过 worker 模拟,但开销巨大)。
- 结果: Webpack 需要 10 秒处理的依赖量,esbuild 可能只需要 0.1 秒。这就是为什么即使它在 dev 启动时运行,你几乎感知不到它的存在。
强大的缓存机制
Vite 的预构建并不是每次 npm run dev 都会全量运行的。
- 本地文件缓存: 预构建的结果会保存在 node_modules/.vite 目录下。
- 判断是否重新构建: Vite 会对比以下几项:package.json 中的依赖字段。package-lock.json(或 yarn.lock/pnpm-lock.yaml)。vite.config.js 中的相关配置。
- 瞬间跳过: 如果这些都没变,Vite 会直接跳过预构建阶段,秒开服务器。
策略差异
Webpack 的痛苦: 它需要把你的业务代码和第三方依赖混在一起进行依赖分析和打包。业务代码天天变,导致它必须反复进行昂贵的计算。
Vite 的精明: 它深知 vue、axios 这些第三方库在开发过程中几乎是不会变的。预构建只针对这些“不动”的库。
业务代码则完全交给浏览器原生 ESM 处理。这种“动静分离”的策略,让 npm run dev 只需要在依赖变化时才进行一次极速的“预洗”。
Vite打包构建
虽然 Vite 在开发时不需要打包,但为了让生产环境的页面加载更快(减少 HTTP 网络往返次数、压缩体积),它在 build 阶段会请出另一位大神:Rollup
环境初始化与配置解析
当输入 npm run build,Vite 首先会确认当前的 Command 是 build 而非 serve。
- 合并配置:将 vite.config.ts 中的生产环境特有配置(如 base 路径、outDir 输出目录、minify 策略)进行最终合并。
- 插件激活:激活那些只在 build 阶段起作用的插件(例如代码压缩插件、离线缓存生成插件)。
利用 Rollup 构建依赖图
Vite 的生产构建完全基于 Rollup。
- 全量扫描:不同于 dev 的按需加载,build 必须从 index.html 开始,顺着入口文件扫描出项目中所有用到的模块。
- 静态分析 (Static Analysis):Rollup 会分析模块间的 import/export 关系。
- Tree Shaking:这是 Rollup 的拿手好戏。它会检查哪些导出的函数从未被使用,并直接在构建图谱中将其剔除。
代码转译与插件流水线
在构建图谱的同时,Vite 会调用相应的插件处理非 JS 资源:
- Vue 编译:将 .vue 文件编译为标准的 JS 渲染函数。
- CSS 提取:Vite 会自动将所有组件用到的 CSS 提取出来,合并成一个或多个独立的 .css 文件,并自动插入 media 查询和压缩。
- 资源处理:图片、字体等静态资源会被处理(小的图片可能会转成 Base64,大的则生成带有 ContentHash 的文件名以便持久化缓存)。
Chunk 生成与分包
Rollup 开始将成百上千个小模块组合成几个大的 Chunks。
- 动态导入 (Dynamic Import):如果你在 Vue 里用了路由懒加载(() => import('./Home.vue')),Rollup 会自动将其识别为一个独立的分包点。
- 公共依赖提取:如果多个页面都用了 axios,Vite 会智能地将 axios 提取到一个公共的 vendor.js 中,避免重复下载。
使用 esbuild 进行极致压缩
这是 Vite 构建效率高于 Webpack 的核心原因之一。
- 逻辑:在 Rollup 生成了最终的 JS bundle 后,Vite 不再使用传统的 Terser 插件进行压缩。
- 底层实现:Vite 默认调用 esbuild 来进行压缩和混淆。
- 性能优势:由于 esbuild 是 Go 语言编写的,处理数兆字节的代码混淆只需毫秒级,而 Webpack 常用的 Terser 需要几秒甚至更久。
物理写盘与 Manifest 生成
- 写入磁盘:将最终优化好的 JS、CSS、HTML 和图片写入 dist 目录。
- 生成资源清单 (Optional):如果配置了 build.manifest: true,Vite 会生成一个 manifest.json 文件,记录了原始文件名与打包后带 Hash 的文件名之间的映射,这在后端集成(如 SSR 或 Django/Laravel 结合)时非常有用。
| 维度 | Webpack (build) | Vite (build) |
| 底层内核 | Webpack 引擎 (JS 编写) | Rollup (针对 ESM 优化) |
| 压缩工具 | Terser (较慢) | esbuild (超快) |
| 产物质量 | 兼容性好,但会有较多冗余代码 | 极度精简,Tree Shaking 彻底 |
| 构建速度 | 较慢(受限于 JS 性能) | 快(得益于 Rollup 和 esbuild) |
| 配置复杂度 | 复杂,需要处理各类 Loader | 简单,生产环境配置高度集成 |
Webpack的HMR和Vite的HMR有什么区别?
Webpack HMR:基于“补丁”的全量依赖追踪
Webpack 的热更新核心逻辑是:在内存中构建 Bundle,并寻找变动模块的“最小补丁”。
底层工作流:
- 构建 Bundle 树:Webpack 启动时,会将所有模块打包成一个或多个巨大的 bundle.js。即使是开发环境,它内部也维护着一套极其复杂的模块依赖图。
- 文件监听与增量编译:当你修改代码,Webpack Compiler 会重新编译。它会对比新旧两次编译的 Hash。
- 计算补丁 (Diff):Webpack 会计算出哪些模块变了,生成两个文件:一个 json(描述变动清单)和一个 js(具体的补丁代码)。
- 冒泡寻主:这是最耗时的。当一个模块变动,Webpack 必须从该模块开始,沿着依赖链向上寻找,直到找到一个能够处理(Accept)该热更新的父模块。如果一直到顶层都没人接收,就只能刷新页面。
Vite HMR
Vite 的热更新逻辑完全不同,它利用了浏览器的原生能力,实现了**“模块解耦”**。
底层工作流:
- No-Bundle 架构:Vite 根本没有把代码揉成一团。浏览器里的每一个 import 都是一个独立的 HTTP 请求,浏览器自己知道谁依赖谁。
- 精准失效通知:当 A.vue 改变时,Vite 服务器通过 WebSocket 发送一个信号,明确告诉浏览器:“A.vue 这个模块失效了,去拿个新的。”
- 动态 Import 重载:浏览器端的 HMR Runtime 收到通知后,直接执行类似 import('/src/A.vue?t=timestamp') 的操作。
- 局部替换:由于是原生 ESM,浏览器只需加载这 一个 变动的模块,并覆盖内存中的旧模块。它不需要像 Webpack 那样去扫描整个依赖图,因为依赖图就在浏览器的请求链路里