vite 的 hmr 快是因为不打包,基于浏览器原生 esm 实现按需编译与模块替换;通过 import.meta.hot 注入轻量更新逻辑,依赖静态导入关系精准传播更新,服务端推送 + 客户端原子 patch,单文件增量转换,响应仅 5–50ms。

Vite 的热更新(HMR)之所以快,核心在于它不打包,直接利用浏览器原生 ESM 加载机制,在开发阶段让模块按需编译、按需替换,避免了传统打包工具的全量重构建。
ESM 动态导入是 HMR 的基础载体
Vite 启动时,会将每个源文件视为一个 ESM 模块,并通过 import.meta.hot 注入 HMR API。这个 API 不是 Vite 自己实现的运行时,而是由开发服务器在响应请求时,对原始源码做轻量级注入(如在文件末尾追加一段 JS),从而让模块具备“可接受更新”的能力。
- 例如,当你写
import { foo } from './utils.js',Vite 实际返回给浏览器的是一个经处理的模块:它保留原始逻辑,同时附加了import.meta.hot.accept()监听逻辑 - 浏览器加载模块时,执行到
import.meta.hot.accept就注册了回调,一旦对应依赖模块变化,Vite 服务端会推送更新消息,客户端触发回调并重新执行该模块代码 - 整个过程不经过 bundler,没有 AST 解析、依赖图重建或 chunk 生成,只做单文件编译(TS/JSX/SFC 等转为浏览器可执行的 JS)和模块替换
模块边界决定更新粒度与传播范围
HMR 更新不是全局刷新,而是基于 ESM 的静态导入关系进行精准传播。Vite 会分析模块的 import 和 export,构建轻量依赖图,仅通知直接受影响的模块及其祖先(需重新执行者)。
- 如果修改的是一个工具函数文件(无副作用、纯导出),且调用方使用了
import.meta.hot.accept('./utils.js', () => {...}),则只重载该模块,不触发父模块执行 - 若未显式 accept,Vite 默认向上冒泡:修改子模块 → 父模块自动重加载 → 继续向上,直到遇到显式
hot.accept()或到达入口(此时触发页面 reload) - 组件框架(如 Vue/React)插件会进一步封装 HMR 逻辑,比如 Vue 单文件组件会把
<script></script>编译为模块,并在accept回调中调用createHotContext来卸载旧组件实例、挂载新实例
服务端推送与客户端 patch 是原子操作
当文件系统监听到变更,Vite 开发服务器不会立刻响应请求,而是:
- 对变更文件执行按需编译(如将
Comp.vue转为 JS 模块字符串) - 计算哪些模块需要被更新(基于已知的 import 关系 + 文件哈希比对)
- 通过 WebSocket 主动向客户端发送 update 消息(含模块路径、新时间戳、新 hash)
- 客户端收到后,发起
GET /@vite/client或直接请求新模块 URL(带 timestamp query 强制绕过缓存),获取新代码 - 执行新模块前,先调用旧模块的
hot.dispose()(如有),再执行新模块,最后触发hot.accept()回调完成局部更新
无打包 ≠ 无转换,但转换是隔离且增量的
Vite 的“原生 ESM”不等于直接 serve 源码。它仍需处理:
- Syntax 转换(TS/JSX/Sass/MD 等 → 浏览器兼容 JS/CSS)
- 路径别名解析(
@/components→src/components) - 环境变量注入(
import.meta.env替换) - 但这些转换都是单文件、内存内、无缓存依赖的——改一个文件,只编译那一个,不读取其他模块内容,也不写磁盘
这套机制让 HMR 响应通常控制在 5–50ms 内,真正做到了“保存即生效”,且与框架解耦,只依赖 ESM 本身的能力。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











