一、Vite 到底解决了什么问题?
我们先回想一下用 Webpack 开发时的体验:每次运行 npm run dev 都要等上半天,哪怕只改一行代码,也要等它重新打包整个项目。项目越大,等待时间越长。
Vite 就是为了解决这个“等待”问题而生的。它的核心思路很简单:为什么要把所有代码都打包好,才让浏览器运行呢?
Vite 发现了一个关键事实:现代浏览器已经原生支持 ES Modules(就是 import/export 语法)。既然浏览器自己能加载模块,那我们就不需要提前把所有代码打包在一起了。
二、开发环境:边用边编译
2.1 启动速度快得惊人
当你运行 vite 命令时,它会做两件事:
- 启动一个本地服务器(比如
localhost:3000) - 预打包你的依赖包(比如 vue、react、lodash 这些从 npm 安装的库)
这里有个聪明的设计:Vite 只预打包 不变的东西(第三方库),不打包 常变的东西(你的业务代码)。因为第三方库很少改动,所以打包一次就可以缓存起来反复用。
这就好比你要做一顿饭:
- 把需要长时间处理的食材(像干货、冷冻品)先预处理好 ✅
- 新鲜的蔬菜现切现炒,保持最佳口感 ✅
2.2 浏览器要什么,Vite 才编译什么
当你在浏览器打开 localhost:3000 时,有趣的事情发生了:
你打开页面 → 浏览器请求 index.html
→ Vite 返回 HTML(偷偷加了热更新脚本)
→ 浏览器请求 main.js
→ 浏览器看到里面有 import App from './App.vue'
→ 浏览器请求 App.vue
→ Vite 现场把 .vue 文件编译成 JavaScript
→ 浏览器执行
关键点:Vite 不会提前编译你的整个项目。它像个“实时翻译官”,浏览器需要哪个文件,它才现场翻译哪个文件。
2.3 路径重写:魔法发生的地方
浏览器不能直接理解 import vue from 'vue' 这种写法(这叫“裸模块导入”)。Vite 在返回文件前会进行重写:
// 浏览器看到的原始代码
import { createApp } from 'vue'
import App from './App.vue'
// Vite 重写后的代码(给浏览器的)
import { createApp } from '/@modules/vue'
import App from './App.vue'
当浏览器请求 /@modules/vue 时,Vite 服务器会拦截这个请求,返回预构建好的 vue 库
2.4 热更新:改了哪里就更新哪里
这是 Vite 最爽的功能。当你修改代码并保存时:
- Vite 立刻重新编译这个文件
- 通过 WebSocket 告诉浏览器:“xxx 文件更新了”
- 浏览器只重新加载这个文件,页面其他部分保持不变
- 你的表单数据、滚动位置都不会丢失
为什么用 WebSocket?而不是直接用http
- HTTP 是“你问我答”,服务器不能主动说话
- WebSocket 是“打电话”,双方可以随时沟通
三、生产环境:还是需要打包
3.1 原因:
- 网络请求太多:一个项目几千个文件,浏览器一个个加载太慢
- 需要优化:删除未使用代码、压缩文件大小、兼容老浏览器
- 静态部署:生产环境通常没有 Node.js 服务器来实时编译
3.2 核心流程
1. 预构建依赖(esbuild)
2. Rollup 打包
3. 输出优化
3.3 详细过程
第一步:依赖预构建
// 用 esbuild(超快)预处理 node_modules
目的:
1. 转换 CommonJS → ESM(让 Rollup 更好处理)
2. 合并小文件
3. 优化 Tree-shaking 基础
第二步:Rollup 打包
// 核心打包阶段
1. 入口分析(从 index.html 开始)
2. 依赖收集(所有 import)
3. 代码转换(.vue→JS, .ts→JS, CSS提取)
4. Tree-shaking(删除未使用代码)
5. 代码分割(按动态 import 分割)
第三步:输出优化
// 生成最终文件
1. 文件命名(添加哈希)
2. 资源处理(图片转 base64/复制)
3. CSS 压缩提取
4. JS 压缩混淆
5. 生成 sourcemap(方便调试时找到源码)
四、Vite vs Webpack:怎么选?
大型项目选 Webpack,中小型选 Vite 的关键原因
4.1 根本区别:架构设计目标不同
Vite:为「速度」设计的「现点现炒」模式
- 核心理念:浏览器要什么,我才编译什么
- 适合场景:模块少、依赖少、追求开发体验
- 致命弱点:项目越大,按需编译的累积成本越高
Webpack:为「规模」设计的「自助餐」模式
- 核心理念:预先准备好一切,运行时直接享用
- 适合场景:模块多、依赖多、追求稳定可靠
- 核心优势:规模越大,预先打包的收益越大
4.2 具体性能拐点(关键数据)
模块数量是决定性因素
Vite 的崩溃点:约 1000 个模块
// 物理限制:浏览器并发请求数有限
6个并发请求 × 每个100ms = 每秒处理60个模块
// 1000个模块 ÷ 60个/秒 = 16.7秒网络等待
// 加上编译时间 → 首次加载超过30秒
// 实际体验:点了页面,等半分钟才出来
Webpack 的稳定区:无上限
// 无论多少模块,都打包成1-5个文件
1-5个请求 × 每个1-3秒 = 3-15秒加载完成
// 规模越大,打包优势越明显
// 10000个模块?还是打包成几个文件
4.3 冷启动:一次性痛苦 vs 持续痛苦
Vite 的「持续小痛苦」
# 每次启动或加新依赖都要:
1. 扫描所有 node_modules
2. 预构建所有依赖包
3. 300个包 × 0.2秒 = 60秒等待
# 改一个依赖包?重新等60秒
# 表面「启动快」,实际「准备久」
Webpack 的「一次性大痛苦」
# 第一次:全量编译(慢)
# 比如:120秒等待
# 之后:
# 1. 依赖包缓存,不再编译
# 2. 只编译业务代码
# 3. 增量编译:0.1-0.5秒
# 痛苦集中在第一次,之后都爽
4.4 热更新:简单查找 vs 复杂依赖链
Vite 的热更新退化
项目结构:
A → B → C → D → E → F → G → H
你修改了 H,Vite 需要:
1. 从 H 开始,一层层问上去
2. "谁用了 H?" → G
3. "谁用了 G?" → F
4. ...一直问到 A
10层依赖?查10次
100层依赖?查100次
越大越慢
Webpack 的热更新稳定
所有模块都在 bundle 里
你修改了 H:
1. 找到 H 在 bundle 中的位置
2. 替换这块代码
3. 通知浏览器更新
无论项目多大,都是三步
速度基本不变
4.5 内存管理:缓存策略决定上限
Vite 的内存炸弹
// 缓存所有编译结果在内存
5000个模块 × 50KB = 250MB
源码 + AST + 依赖图 = 再加500MB
总计:750MB-1.5GB
// Node.js 内存上限:1.4-2GB
// 大型项目频繁内存溢出、卡顿、崩溃
Webpack 的可控内存
// 选择性缓存 + 磁盘缓存
// 可配置内存上限
// 成熟的垃圾回收
// 经过10年企业项目验证
五、Vite 中的插件系统
5.1 Webpack 的对比
在 Webpack 中,有两种扩展方式:
- Loader:处理特定类型文件(如 .vue、.ts)
- Plugin:在打包过程中执行特定任务
在 Vite 中,这两种功能都统一为插件:
| Webpack | Vite | 区别 |
|---|---|---|
| vue-loader | @vitejs/plugin-vue | Vite 插件更集成化 |
| babel-loader | 内置 esbuild 支持 | Vite 内置更多功能 |
| HtmlWebpackPlugin | 内置 HTML 处理 |
5.2 Vite 插件的工作原理
- 转换文件:把 .vue、.ts 等文件转换成浏览器能看懂的 JavaScript
- 扩展功能:比如添加环境变量、配置代理等
5.3 为什么 Vite 配置更简单?
Vite 内置了很多现代前端开发所需的功能:
- ✅ TypeScript 转译(用 esbuild,超快)
- ✅ CSS/SCSS/Less 处理
- ✅ 图片、字体等资源处理
- ✅ JSON 文件自动转换
这些在 Webpack 中都需要单独配置 loader,在 Vite 中都是开箱即用的。
六、热更新
6.1 基础准备阶段
1. 建立双向通信
- 服务器端:Vite 启动时创建 WebSocket 服务器
- 客户端:浏览器加载
/@vite/client脚本,连接到 WebSocket - 结果:建立了实时双向通信通道
2. 构建模块依赖图
- Vite 在内存中建立一张「模块关系地图」
- 记录每个文件的:谁导入了我?我导入了谁?
- 这张图是热更新的「导航地图」
6.2 热更新触发流程(五步走)
第一步:文件保存事件
你修改了 App.vue 并保存
↓
操作系统通知 Vite:App.vue 文件改变了
↓
Vite 收到文件变化事件
第二步:重新编译文件
Vite 发现 App.vue 变了
↓
立即重新编译 App.vue:
.vue 文件 → Vue 编译器 → 纯 JavaScript
↓
得到新的 JavaScript 代码
第三步:查找更新边界(核心步骤)
场景示例:
假设你的项目结构:
App.vue (入口)
└─ Home.vue
└─ Header.vue
└─ Button.vue
└─ utils.js (你修改了这个文件)
查找过程:
Vite 问:谁导入了 utils.js?
答案:Button.vue 导入了它
Vite 问:Button.vue 是否声明接受 utils.js 的更新?
如果是 → Button.vue 就是「边界」,停止查找
如果不是 → 继续向上找
Vite 问:谁导入了 Button.vue?
答案:Header.vue 导入了它
...如此一层层向上查找,直到:
1. 找到声明「我接受这个更新」的模块(边界)
2. 或者找到最顶层的入口
这个过程叫「查找 HMR 边界」
第四步:发送更新通知
找到需要更新的模块后
↓
Vite 通过 WebSocket 发送消息给浏览器:
"Hey,Button.vue 需要更新"
↓
消息包含:哪个文件、更新时间戳等
第五步:浏览器执行更新
浏览器收到消息
↓
重新请求新的 Button.vue(带时间戳避免缓存)
↓
执行之前注册的「热更新回调函数」
↓
页面局部更新,保持状态
6.3 关键概念解释
1. 什么是「HMR 边界」?
- 边界模块:声明自己可以接受热更新的模块
- 作用:防止无限制的重新加载
- 示例:
// 在 Button.vue 中: if (import.meta.hot) { import.meta.hot.accept() // ← 声明自己是边界 } // 这样 utils.js 更新时,只需要更新 Button.vue // 不需要重新加载 Header.vue、Home.vue、App.vue
2. 为什么需要「查找边界」?
- 没有边界:改一个小工具函数 → 重新加载整个应用
- 有边界:改一个小工具函数 → 只更新使用它的组件
- 目的:最小化更新范围,保持页面状态
6.4 不同类型文件的处理方式
1. Vue 组件更新
修改 App.vue 的模板
↓
Vite 重新编译模板为 render 函数
↓
只替换组件的 render 函数
↓
Vue 重新执行 render,更新虚拟DOM
↓
保持 data、props、状态不变
2. CSS 样式更新
修改 style.css
↓
Vite 重新编译 CSS
↓
直接替换页面的 <style> 标签内容
↓
页面样式立即更新,无需刷新
3. JavaScript 模块更新
修改 utils.js 工具函数
↓
Vite 重新编译
↓
重新执行这个模块
↓
调用之前注册的更新回调
6.5 总结流程
你修改文件 → 保存
↓
Vite 检测到变化
↓
重新编译这个文件
↓
查找「谁需要知道这个变化」
↓
通过 WebSocket 通知浏览器
↓
浏览器重新加载更新的模块
↓
执行热更新回调
↓
页面局部更新 ✅
核心思想:不是刷新整个页面,而是找到最小的更新单元,只更新那一部分,保持应用状态不变。
七、一些技术细节
7.1 为什么第三方库要预打包?
因为很多库用的是 CommonJS 格式(require/module.exports),浏览器不认识。而且一个库可能包含几百个小文件,让浏览器一个个加载会慢到哭。
7.2 Vite 怎么处理 .vue、.ts 这些特殊文件?
Vite 有一系列插件,比如 @vitejs/plugin-vue 专门处理 .vue 文件。当浏览器请求这些文件时,对应的插件会把它们“翻译”成浏览器能看懂的 JavaScript。
7.3 热更新怎么知道更新哪里?
Vite 在内存里维护了一张“依赖关系图”,记录每个文件被谁引用。当某个文件变化时,它就顺着这张图找到需要更新的最小范围。
总结
Vite 的核心创新是开发和生产用两套策略:
- 开发:利用浏览器原生能力,按需编译,快速响应
- 生产:回归传统打包,保证性能优化
它的出现不是要完全取代 Webpack,而是给了我们一个新的选择。对于大多数现代前端项目(特别是新项目),Vite 能提供好得多的开发体验。但如果你在做的是一个非常庞大、需要深度定制的企业级应用,Webpack 可能还是更稳妥的选择。