跳到主要内容

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 的核心不是一个单点优化,而是把传统打包器在开发阶段承担的重活拆开:

  1. 依赖预构建:第三方依赖先处理好,避免浏览器发太多碎请求,也顺便把 CommonJS 等格式转成 ESM。
  2. 源码按需转换:浏览器请求哪个模块,服务端就只转换哪个模块。
  3. 模块图驱动缓存与失效:不是“整个项目重编译”,而是“只让相关模块失效”。
  4. 开发与生产分治:开发追求启动快、更新快;生产追求包体积、缓存命中率和部署稳定性。

如果把这 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 处理一个模块请求时,通常会经过一条插件驱动的管线。你可以把它理解成“轻量版编译流水线”:

  1. resolve:把导入路径解析成真实文件或虚拟模块。
  2. load:读取源码内容,或者直接生成虚拟模块内容。
  3. transform:做语法转换、注入 HMR 代码、重写 import、产出 source map。

这也是为什么 Vite 插件生态很重要。它并不只是个静态文件服务器,而是一个插件化的模块转换服务器

4.1 为什么 .vue.tsx、CSS 都能直接 import?

因为浏览器本身不认识这些源文件格式,真正让它们“能跑”的,是 Vite 在返回响应前已经做了转换:

  • .vue 被拆成脚本、模板、样式模块
  • .tsx / JSX 被转成浏览器可执行的 JS
  • CSS 会被转换为可注入页面并参与 HMR 的模块

浏览器看到的是转换后的 ESM,不是原始源文件能力突然变强了。

5. 模块图:Vite 为什么能精准失效?

Vite 在开发服务器内存里维护一张模块图。你可以简单理解为:

  • 节点:模块
  • 边:导入关系
  • 附加信息:谁导入我、我依赖谁、转换缓存是否有效

这张图至少解决了 3 个问题:

  1. 缓存复用:没变的模块不重复转换。
  2. 失效传播:某个文件改了,只把受影响模块标记失效。
  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 做正式打包优化