How does it achieve "extremely fast HMR".
🚀 1. Why is Vite’s HMR so fast?
The core idea of Vite:
🧠 There is no need to rebuild the bundle, but the actual changed files are updated in a targeted manner
Webpack:
- File changed → Webpack rebuilds part of the bundle → Send patch → Replace module
Vite:
- The file has been changed → The browser re-requests the ESM version of this file
- If there are dependencies → only update the dependency chain, not package
So the fundamental reason why Vite HMR is fast: Natural ESM, no bundle, no diff, no patch.
📦 2. HMR core module structure
Table of contents (approximately)
vite/
├─ server/
│ ├─ index.ts → 创建 dev server(核心)
│ ├─ ws.ts → WebSocket 通信
│ ├─ hmr.ts → HMR 的依赖跟踪 & 更新逻辑
│ ├─ moduleGraph.ts → 模块依赖图(最关键)
│ ├─ plugins/
│ ├─...
│ ├─ transformRequest.ts → 单文件的编译/转换
│ └─...Core components:
- ModuleGraph module graph (tracking module dependencies)
- Watcher file listener (chokidar)
- WS WebSocket System (push update events to the browser)
- Hot update handler: server.handleHMRUpdate()
- Client HMR Runtime (/vite/client)
🕸 3. ModuleGraph - the brain of HMR
Vite maintains a graph structure for each module:
class ModuleNode {
url: string
importedModules: Set<ModuleNode>
importers: Set<ModuleNode>
}Example:
A.js → import B.js
B.js → import C.jsModuleGraph will log:
- B's importers =
- C's importers =
➡️ When C changes → B needs to be notified ➡️ If B does not have an HMR receiver → Notify A again (bubbling up)
📡 4. After file changes (HMR entire link)
Now comes the most critical part: How does HMR really move?
✨ Step 1: Monitor file changes (chokidar)
In server/index.ts:
watcher.on('change', (file) => {
server.handleHMRUpdate(file)
})When you edit a file, such as src/App.vue:
File changed: src/App.vue✨ Step 2: handleHMRUpdate() handles updates
Key function: server.handleHMRUpdate(file)
It will:
- Find the corresponding ModuleNode
- Execute the module's plugin
hmrhook (e.g. Vue plugin) - Collect modules in the dependency chain that accept updates
- Send update events via WebSocket
✨ Step 3: Determine which modules "accept updates"
HMR has two processing methods:
✔ There is import.meta.hot.accept()
→ Partial hot update (only refresh relevant modules)
❌ No HMR processor
→ Reverse search for “importers” based on dependencies → Find the nearest HMR boundary → If not found → Fully refresh the page
For example:
A.vue → import B.js → import C.jsIf C.js is changed:
Does C.js accept? ❌ No
Does its importer B.js accept it? ❌ No
Look up again A.vue ✔ HMR accept with Vue
➡️Finally update A.vue (trigger Vue partial update)
✨ Step 4: WebSocket Push Update Message
Code from ws.ts:
ws.send({
type: 'update',
updates: [{ path, timestamp }]
})Vite uses ws://localhost:5173 to create a WebSocket, which only sends JSON:
{
"type": "update",
"updates": [
{
"path": "/src/components/MyComp.vue",
"timestamp": 1679999999999
}
]
}✨ Step 5: Browser side (client HMR runtime)
The HMR client runs at:
vite/clientMain logic:
- Receive WebSocket push
- Dynamically call
import(path + "?t=timestamp")based onpath - Replace module
- Execute
import.meta.hot.accept()callback
Example (simplified):
socket.onmessage = async (msg) => {
if (msg.type === 'update') {
for (const update of msg.updates) {
import(update.path + '?t=' + update.timestamp)
}
}
}Very straightforward: Just re-import the file!
No patches, no diffs, no bundles - so fast.
✨ Step 6: If no module accepts the update → Full page refresh
location.reload()This is why sometimes HMR fails and automatically refreshes the page.
🧩 5. Why is Vite so fast?(Summary)
Because:
- Use browser native ESM (No Bundle)
- Whichever file is changed will be updated.
- HMR is an accurate update based on dependency graph
- The client only needs to re-import the new module
- No patch, no rebuild, no pre-bundle required
This is the fundamental reason why Vite can be hot updated in seconds.