Vite 的 Module Graph + HMR 源码级解析

0 阅读3分钟

一、Module Graph(模块图)深入解析

ChatGPT Image 2026年3月17日 08_41_29.png

Vite 内部维护了一个叫 ModuleGraph 的数据结构,用于记录 模块之间的依赖关系

每个模块节点结构大概如下(TypeScript 风格):

interface ModuleNode {
  url: string               // 模块路径,例如 /src/App.vue
  importedModules: Set<ModuleNode>  // 当前模块依赖的模块集合
  importers: Set<ModuleNode>        // 依赖当前模块的模块集合
  lastHMRTimestamp: number          // 上次 HMR 更新时间
}

⚡ 核心作用:

  1. 精准定位依赖
    当某个模块变更时,Vite 只更新依赖它的模块,而不是全量重载。
  2. 按需编译
    浏览器请求模块时,Vite 可以直接从 ModuleGraph 找到依赖链,按需返回编译结果。
  3. 缓存管理
    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 能这么快

  1. SFC 编译拆分

Vite 对 .vue 文件按类型拆分:

  • <script> → JS Module
  • <template> → render 函数
  • <style> → JS style module

浏览器只更新修改的部分。

  1. ESM 按需加载

浏览器直接请求:

/src/App.vue?type=template
/src/Button.vue?type=script

不再重新打包整个 bundle。

  1. 缓存 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 架构本质总结

  1. 浏览器驱动
    浏览器请求模块 → Vite 按需 transform → 返回 ESM
    → 启动秒开
  2. 模块图 + 精准 HMR
    ModuleGraph 记录依赖 → 变更只更新受影响模块
    → 刷新毫秒级
  3. 依赖预构建
    esbuild 快速处理 node_modules → 避免大量 HTTP 请求
    → 加载快
  4. 缓存策略
    多层缓存(deps / transform / HTTP) → 避免重复编译
  5. 生产环境仍用 Rollup
    → 保证最终 bundle 最优、Tree Shaking 完整