一、Module Graph(模块图)深入解析
Vite 内部维护了一个叫 ModuleGraph 的数据结构,用于记录 模块之间的依赖关系。
每个模块节点结构大概如下(TypeScript 风格):
interface ModuleNode {
url: string // 模块路径,例如 /src/App.vue
importedModules: Set<ModuleNode> // 当前模块依赖的模块集合
importers: Set<ModuleNode> // 依赖当前模块的模块集合
lastHMRTimestamp: number // 上次 HMR 更新时间
}
⚡ 核心作用:
- 精准定位依赖
当某个模块变更时,Vite 只更新依赖它的模块,而不是全量重载。 - 按需编译
浏览器请求模块时,Vite 可以直接从 ModuleGraph 找到依赖链,按需返回编译结果。 - 缓存管理
transform 结果、预构建结果都会关联到节点,保证热更新高效。
二、HMR 流程深度解析
假设项目结构:
App.vue
├── Button.vue
└── utils.ts
我们修改 Button.vue。源码级流程大概是:
1️⃣ 文件监听
Vite 使用 chokidar 监听文件变动:
fsWatcher.on('change', (file) => handleFileChange(file))
2️⃣ 找到 ModuleGraph 节点
const node = moduleGraph.getModuleByUrl('/src/Button.vue')
3️⃣ 查找 importers(受影响模块)
const affectedModules = moduleGraph.getAffectedModules(node)
这里的 getAffectedModules 会沿着依赖链向上查找所有模块。
4️⃣ 调用 transform
const transformed = await pluginContainer.transformRequest('/src/Button.vue')
- Vue SFC → JS Module
- TypeScript → JS
- CSS/LESS → JS module
5️⃣ WebSocket 推送到浏览器
ws.send({
type: 'update',
updates: [
{ path: '/src/Button.vue', timestamp, type: 'js-update' }
]
})
浏览器接收到消息:
import.meta.hot.accept('/src/Button.vue', (mod) => {
// 重新渲染组件
})
✅ 整个流程只影响 Button.vue 和依赖它的模块,所以 HMR 通常在 20~50ms 内完成。
三、为什么 Vue 组件 HMR 能这么快
- SFC 编译拆分
Vite 对 .vue 文件按类型拆分:
<script>→ JS Module<template>→ render 函数<style>→ JS style module
浏览器只更新修改的部分。
- ESM 按需加载
浏览器直接请求:
/src/App.vue?type=template
/src/Button.vue?type=script
不再重新打包整个 bundle。
- 缓存 transform 结果
- 如果
<style>未修改 → 不会重新编译 - 如果
<script>未修改 → 直接用缓存
四、node_modules 依赖预构建机制
浏览器原生 ESM 不支持 CommonJS,所以 Vite 会 提前编译依赖:
// esbuild 预构建
esbuild.build({
entryPoints: deps,
format: 'esm',
outdir: '.vite/deps',
bundle: true
})
结果:
.vite/deps/
vue.js
vue-router.js
lodash.js
- 浏览器只请求一次,避免 500+ HTTP 请求
- HMR 时也会用缓存,极快
五、源码级缓存策略
Vite Dev Server 内部有三层缓存:
1️⃣ 依赖预构建缓存
- 存放在
.vite/deps - node_modules 不变时直接复用
2️⃣ transform 缓存
- 源码 → 编译 → 缓存 transform 结果
- 避免重复调用 esbuild / vue compiler
3️⃣ 浏览器缓存
- ETag / 304 Not Modified
- 减少网络传输
⚡ 组合效果:启动秒开、刷新毫秒级。
六、对比 Webpack 的瓶颈
Webpack HMR 流程:
修改模块
↓
Webpack rebuild
↓
计算 Chunk
↓
生成 bundle
↓
浏览器更新
瓶颈:
- 全量 rebuild
- Chunk 计算复杂
- 浏览器只接收 bundle,无法按模块更新
即使用 DLL / cache-loader / thread-loader,也无法从根本上改变:
架构是 bundle-first,而不是浏览器驱动。
七、Vite 架构本质总结
- 浏览器驱动
浏览器请求模块 → Vite 按需 transform → 返回 ESM
→ 启动秒开 - 模块图 + 精准 HMR
ModuleGraph 记录依赖 → 变更只更新受影响模块
→ 刷新毫秒级 - 依赖预构建
esbuild 快速处理 node_modules → 避免大量 HTTP 请求
→ 加载快 - 缓存策略
多层缓存(deps / transform / HTTP) → 避免重复编译 - 生产环境仍用 Rollup
→ 保证最终 bundle 最优、Tree Shaking 完整