文章

为什么 Vite 能这么快:从 Webpack Dev 到原生 ESM

为什么 Vite 能这么快:从 Webpack Dev 到原生 ESM

注:
本专栏基于作者当前阶段的理解进行整理与表达。
如有疏漏或不准确之处,欢迎反馈与交流:Issues
在此表示感谢。

引言:Vite 究竟快在哪里?

在Vite出现之前,开发者对于“启动一个前端项目”的心理预期往往是:

运行项目 → 等待一次完整构建 → 页面呈现

而 Vite 的出现,第一次让人产生了一种反直觉的体验:项目几乎是秒启动的,并且热更新也非常的快。这种体验差异并不是来自某个单点优化,而是源于 Vite 对“开发阶段构建模型”的彻底重构

要确切理解Vite如此“快”缘由,关键不在于理解“它做了什么”,而在于理解相对于传统构建工具而言,它“不做了什么”。


一、Webpack Dev的设计初衷与成功模型

在进入Vite之前,我们首先需要明晰传统构建工具的设计初衷。 Webpack、Rollup等传统构建工具从设计上并不服务于“快启动”,而是为了:

  • 生成可部署的生产代码
  • 兼容复杂的模块系统
  • 在一次构建中完成所有分析、转换与合并

它们的设计理念在于:

在构建期实现一个完整的模块系统,并在生产环境运行时执行

这在生产环境中是极其成功的。但对于开发阶段而言,这也意味着“启动即全量构建”,这在项目较大时会导致启动时间特别的长。


以Webpack为例,在执行npm run dev时,通常会发生:

  1. 从入口开始解析依赖
  2. 构建完整的模块依赖图
  3. 对所有模块进行 loader / plugin 处理
  4. 生成一个(或多个)bundle
  5. 启动 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!看一下作者的其他开源项目!

👉 @ws-flow/client

一个面向事件流的 WebSocket 客户端框架,尝试把通信过程组织为一种可组合的“流”结构。

  • 将不同通信方式(请求 / 推送)统合到同一模型之中
  • 通过事件订阅与规则路由从数据流中获取数据
  • 以流式任务的方式处理持续到达的数据
  • 构建可拓展的中间件管线灵活调整通信逻辑

如果你正在做实时系统或复杂通信层,可能对你有所帮助。

本文由作者按照 CC BY 4.0 进行授权

© . 保留部分权利。

本站采用 Jekyll 主题 Chirpy