Vite 核心工作原理
很多人背 Vite,只记住一句“它很快”。这在面试里不够。更稳的答法是:Vite 不是简单把 Webpack 做快了,而是重新切分了开发阶段和生产阶段的职责。
0. 面试速答(30 秒版 TL;DR)
- 开发态:Vite 不先把整个应用打成 bundle,而是把浏览器当成模块加载器,基于原生 ESM 做按需请求、按需转换。
- 冷启动快:源码文件不需要全量构建;依赖包会先做一次预构建(pre-bundling),把大量裸模块导入转成浏览器更容易消费的 ESM 产物。
- 更新快:Vite 维护模块图,文件变化后只让受影响模块失效,通过 WebSocket 通知浏览器做局部热更新。
- 生产构建:开发态和生产态不是同一套路径。Vite 开发时像 Dev Server,生产时通常交给 Rollup 做打包、切 chunk、Tree Shaking 和产物优化。
说明:本文描述的是 Vite 主流版本下稳定存在的通用机制,细节实现可能随版本演进,但核心模型长期稳定。
1. 先建立心智模型:Vite 把“快”拆成了 4 件事
Vite 的核心不是一个单点优化,而是把传统打包器在开发阶段承担的重活拆开:
- 依赖预构建:第三方依赖先处理好,避免浏览器发太多碎请求,也顺便把 CommonJS 等格式转成 ESM。
- 源码按需转换:浏览器请求哪个模块,服务端就只转换哪个模块。
- 模块图驱动缓存与失效:不是“整个项目重编译”,而是“只让相关模块失效”。
- 开发与生产分治:开发追求启动快、更新快;生产追求包体积、缓存命中率和部署稳定性。
如果把这 4 件事讲清楚,Vite 的核心原理基本就立住了。
2. 开发态主链路:浏览器发请求,Vite 才做工作
先看主链路,只保留面试里最该讲清楚的 5 个动作:
图里真正要背的是:浏览器按需请求,Vite 按需转换;第三方依赖和业务源码走两条不同路径。
2.1 为什么冷启动快?
因为它避免了传统“启动前先把全项目打包一遍”的路径。
传统打包器在开发态的常见做法是:
- 先从入口递归扫完整张依赖图
- 再把很多模块打进 bundle
- 浏览器最后拿到一个或少数几个产物
Vite 改成了:
- 入口 HTML 先返回
- 浏览器开始按
import链路请求模块 - 服务端只在“被请求时”才去转换对应模块
所以初次启动成本更接近“首屏实际用到多少模块”,而不是“整个项目总共有多少模块”。
3. 为什么还需要依赖预构建?
很多人听到“按需加载”后会误以为 Vite 完全不打包,这是不准确的。Vite 对业务源码尽量不提前打包,但对第三方依赖会做预构建。
原因主要有两个:
3.1 兼容性问题
浏览器直接理解的是 ESM,但很多 npm 包发布出来不一定是浏览器可直接消费的形态,比如:
- CommonJS
- 多层内部依赖拆分严重的包
- 同时暴露多种入口格式的包
预构建会把这些依赖整理成浏览器更容易加载的 ESM 结果。
3.2 请求数量问题
如果一个依赖自己又依赖几十上百个小模块,浏览器首次加载会打出很多请求。预构建可以把这种“过碎的依赖树”做一次整理,减轻开发时的请求瀑布。
面试里可以直接说:
- Vite 不是完全不做 bundling,而是把 bundling 收缩到第三方依赖上。
- 业务源码按需转换,依赖做预构建,这是它开发态体验好的关键。
4. 请求到响应之间发生了什么?
Vite 处理一个模块请求时,通常会经过一条插件驱动的管线。你可以把它理解成“轻量版编译流水线”:
- resolve:把导入路径解析成真实文件或虚拟模块。
- load:读取源码内容,或者直接生成虚拟模块内容。
- transform:做语法转换、注入 HMR 代码、重写 import、产出 source map。
这也是为什么 Vite 插件生态很重要。它并不只是个静态文件服务器,而是一个插件化的模块转换服务器。
4.1 为什么 .vue、.tsx、CSS 都能直接 import?
因为浏览器本身不认识这些源文件格式,真正让它们“能跑”的,是 Vite 在返回响应前已经做了转换:
.vue被拆成脚本、模板、样式模块.tsx/ JSX 被转成浏览器可执行的 JS- CSS 会被转换为可注入页面并参与 HMR 的模块
浏览器看到的是转换后的 ESM,不是原始源文件能力突然变强了。
5. 模块图:Vite 为什么能精准失效?
Vite 在开发服务器内存里维护一张模块图。你可以简单理解为:
- 节点:模块
- 边:导入关系
- 附加信息:谁导入我、我依赖谁、转换缓存是否有效
这张图至少解决了 3 个问题:
- 缓存复用:没变的模块不重复转换。
- 失效传播:某个文件改了,只把受影响模块标记失效。
- HMR 定位:沿着 importer 链往上找能接住更新的边界。
所以 Vite 的快,不是因为“每次变更都算得很快”,而是因为尽量不算无关的东西。
6. HMR 为什么通常很快?
Vite 的热更新链路可以压缩成一句话:
- 文件变更 -> 模块图定位影响范围 -> WebSocket 发通知 -> 浏览器重新拉新模块 -> 边界内局部替换
这里最关键的是:HMR 不需要重新生成整包 bundle。浏览器只需要重新请求少量模块。
常见面试追问:
为什么有时会退化为整页刷新?
因为不是所有模块都能安全热替换。若沿导入链路向上找不到能接收更新的边界,或者框架判定此次变更不可安全替换,就会退化为 full reload。
7. 生产构建为什么又要回到打包?
开发态和生产态目标不同。
开发态更关心:
- 快速启动
- 快速更新
- 易调试
生产态更关心:
- 减少请求数
- 长缓存与文件 hash
- Tree Shaking
- 代码分割
- 资源压缩
这些事情天然更适合交给成熟 bundler 来做,所以 Vite 在生产构建阶段通常使用 Rollup:
- 根据入口构建完整依赖图
- 做 chunk 切分
- 做静态分析和摇树优化
- 产出适合部署的静态资源
这也是一个很容易被问到的点:
- Vite 不是“只靠浏览器 ESM 就能取代生产打包”。
- 浏览器 ESM 很适合开发态,但线上交付仍然需要构建产物优化。
8. Vite 和传统打包器的本质差异
很多人会把问题答成“Vite 比 Webpack 快,因为它用 esbuild”。这只说对了一小部分。
更完整的答法是:
| 维度 | 传统打包器开发态 | Vite 开发态 |
|---|---|---|
| 核心思路 | 先构建 bundle,再交给浏览器 | 浏览器直接按 ESM 请求,服务端按需转换 |
| 冷启动成本 | 更接近全项目依赖图规模 | 更接近首屏实际访问模块规模 |
| 依赖处理 | 和业务代码一起走构建流程 | 第三方依赖预构建,业务源码按需处理 |
| 更新路径 | 常涉及 bundle 重建 | 常只涉及受影响模块重新请求 |
| 生产构建 | 自己完成 | 通常交给 Rollup |
9. 典型题与标准答法
9.1 为什么 Vite 冷启动快?
标准答法:
因为它开发态不先整包构建,而是基于浏览器原生 ESM 按需加载模块。业务源码在被请求时才转换,第三方依赖只做一次预构建,所以启动成本主要落在“首屏会访问到的模块”上,而不是整个项目全部模块。
9.2 Vite 为什么还需要 esbuild 和 Rollup?
标准答法:
Vite 本身更像一套工具链调度者。开发态会用 esbuild 做依赖预构建和一些高性能转换;生产态通常交给 Rollup 做正式打包、切 chunk 和产物优化。它不是只依赖单一底层引擎。
9.3 Vite 的快主要快在编译器速度吗?
标准答法:
不只如此。编译器速度当然重要,但更关键的是架构变化:开发态不整包打包、浏览器按需请求、服务端按需转换、模块图精准失效。这些机制共同决定了它的体验。
10. 常见误区
- 误区 1:Vite 完全不打包。 错。开发态尽量不对业务源码整包打包,但依赖会预构建,生产构建也会正式打包。
- 误区 2:Vite 快只是因为用了 esbuild。 不完整。真正决定体验的是开发架构改造,而不是只换了一个更快的转译器。
- 误区 3:浏览器原生 ESM 既然能开发,就没必要线上打包。 错。开发态和生产态优化目标不同。
- 误区 4:Vite 是 Rollup 的皮。 也不准确。生产构建默认深度依赖 Rollup,但开发态核心模型完全不同。
11. 速记要点(可背)
- Vite 开发态 = 浏览器原生 ESM + 服务端按需转换
- Vite 冷启动快 = 不先整包构建业务源码
- 依赖预构建 = 兼容格式 + 减少碎请求
- HMR 快 = 模块图精准失效 + 浏览器局部重新请求
- 生产构建 = 通常交给 Rollup 做正式打包优化