为什么 Vite 能这么快:从 Webpack Dev 到原生 ESM
注:
本专栏基于作者当前阶段的理解进行整理与表达。
如有疏漏或不准确之处,欢迎反馈与交流:Issues
在此表示感谢。
引言:Vite 究竟快在哪里?
在Vite出现之前,开发者对于“启动一个前端项目”的心理预期往往是:
运行项目 → 等待一次完整构建 → 页面呈现
而 Vite 的出现,第一次让人产生了一种反直觉的体验:项目几乎是秒启动的,并且热更新也非常的快。这种体验差异并不是来自某个单点优化,而是源于 Vite 对“开发阶段构建模型”的彻底重构。
要确切理解Vite如此“快”缘由,关键不在于理解“它做了什么”,而在于理解相对于传统构建工具而言,它“不做了什么”。
一、Webpack Dev的设计初衷与成功模型
在进入Vite之前,我们首先需要明晰传统构建工具的设计初衷。 Webpack、Rollup等传统构建工具从设计上并不服务于“快启动”,而是为了:
- 生成可部署的生产代码
- 兼容复杂的模块系统
- 在一次构建中完成所有分析、转换与合并
它们的设计理念在于:
在构建期实现一个完整的模块系统,并在生产环境运行时执行
这在生产环境中是极其成功的。但对于开发阶段而言,这也意味着“启动即全量构建”,这在项目较大时会导致启动时间特别的长。
以Webpack为例,在执行npm run dev时,通常会发生:
- 从入口开始解析依赖
- 构建完整的模块依赖图
- 对所有模块进行 loader / plugin 处理
- 生成一个(或多个)bundle
- 启动 dev server 提供访问
无论你是否马上访问某个页面,这一步几乎是不可跳过的。因此我们可以很容易发现,Webpack之所以启动“慢”,问题的根源就在于:
即使是dev,Webpack依然在做“全量构建”。
即使在热更新(HMR)阶段,Webpack仍需要从变更的模块开始:
- 重新生成模块AST
- 重新计算受影响的依赖图
- 重新生成受影响的 bundle
- 通过 runtime 与浏览器交互,实现模块热替换
HMR已经十分成熟,但它依然建立在:
bundle 是基本单位 依赖关系由构建工具维护
这个前提之上。因此将Webpack Dev的问题总结为一句话就是:
Webpack在Dev阶段依然使用了“生产级别的构建模型”。
而这个模型的构建成本,天然与项目规模强相关。
二、浏览器原生ESM:Dev 阶段的新基础
到这里为止,我们会天然问出一个问题:
Vite是如何在Dev阶段规避掉上述问题的?
这里我们可以重新聚集回开头的问题,“Vite不做了什么?”,直接给到答案:
Vite在Dev阶段直接选择了“不打包”
这基于一个重要前提: 现代浏览器已经原生支持 ESM
1
2
// 浏览器已经可以直接理解
import { add } from './math.js'
这意味着浏览器已内建了一整套模块系统,当遇见ES Module时,浏览器会:
- 请求入口模块
- 在执行前解析import语句
- 递归请求依赖模块
- 构建模块依赖图
- 缓存模块实例
从逻辑上来看,这与bundler构建模型几乎是等价的,只是执行者从“构建工具”变成了“浏览器”。因此在原生ESM出现之后,构建工具在Dev阶段就已不再需要为浏览器实现模块通信能力了。
三、Vite在开发阶段到底做了什么?
然而尽管浏览器的能力已十分到位,但在实际项目面前却也仍存在着一些重要的问题:
- npm 包大量仍是 CommonJS
- 裸模块路径浏览器无法解析
- TS / JSX / Vue SFC 无法直接执行
- 工程代码并非浏览器可直接消费的形态
简单来说就是:
浏览器的能力足够,但输入并不合法。
因此Vite在Dev阶段的核心策略大致可以概况为一句话:
让浏览器接管模块系统,构建工具负责“翻译”和“按需供给”
具体来说就是:不打包模块、不构建模块依赖图,也不生成bundle。 而是:
- 启动一个 dev server
- 拦截浏览器的模块请求
- 将对应模块的源码即时转换为浏览器可执行的 ESM
- 重写 import 路径,使其符合浏览器的模块请求语义
- 在模块级别实现 HMR
1
2
3
4
import { ref } from 'vue'
// 路径重写
import { ref } from '/node_modules/.vite/vue.js'
因此在Vite中,只有真正被浏览器请求到的模块才会被编译处理。这与传统bundler的差异非常关键:
| 模型 | 何时处理模块 |
|---|---|
| Webpack | 启动时几乎处理全部 |
| Vite | 浏览器请求到时才处理 |
快的根本原因不是“处理更快”,而是“处理更少”。Vite只做“最小必要加工”。
依赖预构建: Vite唯一的“主动构建”
需要特别注意的是,Vite并非完全不做构建行为。在首次启动的时候,Vite会对项目中的第三方依赖进行一次预处理:
- 使用esbuild,将 CommonJS 转换为 ESM
- 合并碎片化依赖
- 缓存到
.vite目录
这一步的目的在于:
减少浏览器请求数量,降低dev阶段的系统开销
重要边界在于:
预构建只针对依赖,不针对业务模块
这也是为什么Vite在首次启动时会花费一些时间,而后续的HMR则快得多的原因。
四、为什么生产环境仍需要打包
到这里为止,我们自然而然会存在一个疑问:
既然Dev阶段可以不打包,为什么在生产环境还要Rollup?
答案很明显:开发目标与生产目标不同。
在开发阶段我们重点关注的是:
- 项目启动速度
- HMR模块热更新体验
- 开发与调试友好性
而到了生产环境中,我们则更关注:
- 网络请求数量
- 代码体积
- 运行时性能
- Tree-Shaking
在生产环境中,模块加载的成本不再是“是否能加载”,而是“加载多少次、加载多大、加载多慢”。
而要实现这些目标,依然需要借助Rollup实现:
- 模块合并
- 作用域提升
- 静态优化
因此:
Vite = Dev Server (ESM + 按需) + Rollup (生产打包)
结语: Vite的核心定位
到这里为止,相信我们已经对Vite有了更为深入的认识。Vite 并没有发明新的构建模型,它真正做的事情是:
将“构建”这件事拆分为两个阶段,并为每个阶段选择最合适的执行者
- 开发阶段:由浏览器主导模块系统,构建工具只做最小必要加工
- 生产阶段:由构建工具接管全局视角,完成极致的静态优化
Vite 的快,并不是因为它“比 Webpack 更努力”,
而是因为它终于意识到:
在开发阶段,构建工具不必再替浏览器做浏览器已经会做的事
这正是 Vite 能够“重新定义前端开发体验”的根本原因。
🚀 Hello!看一下作者的其他开源项目!
一个面向事件流的 WebSocket 客户端框架,尝试把通信过程组织为一种可组合的“流”结构。
- 将不同通信方式(请求 / 推送)统合到同一模型之中
- 通过事件订阅与规则路由从数据流中获取数据
- 以流式任务的方式处理持续到达的数据
- 构建可拓展的中间件管线灵活调整通信逻辑
如果你正在做实时系统或复杂通信层,可能对你有所帮助。