Vite 底层所深度使用的两个构建引擎—— Esbuild 和 Rollup 。但很多人对 Vite 的双引擎架构仅仅停留在 开发阶段使用 Esbuild,生产环境用 Rollup 的阶段,Vite 真正的架构远没有这么简单

Esbuild 在很多关键的构建阶段让 Vite 获得了相当优异的性能,如果这些阶段用传统的打包器/编译器来完成的话,开发体验要下降一大截
首先是开发阶段的依赖预构建阶段。一般来说 node_modules 依赖的大小几百 MB 甚至上 GB ,会远超项目源代码。如果这些依赖直接在 Vite 中使用,会出现一系列的问题,主要是 ESM 格式的兼容性问题和海量请求的问题。
总而言之,对于第三方依赖,需要在应用启动前进行打包并且转换为ESM 格式。Vite 1.x 版本中使用 Rollup 来做这件事情,但 Esbuild 的性能实在是太恐怖了,Vite 2.x 果断采用 Esbuild 来完成第三方依赖的预构建,参照它与传统打包工具的性能对比图:

当然 Esbuild 作为打包工具也有一些缺点
尽管 Esbuild 作为一个社区新兴的明星项目,有如此多的局限性,但依然不妨碍 Vite 在 开发阶段使用它成功启动项目并获得极致的性能提升,生产环境出于稳定性考虑当然是采用功能更加丰富、生态更加成熟的 Rollup 作为依赖打包工具了
在 TS(X)/JS(X) 单文件编译上面,Vite 也使用 Esbuild 进行语法转译,也就是将 Esbuild 作为 Transformer 来用。可以在架构图中 Vite Plugin Pipeline 部分注意到:

Esbuild 转译 TS 或者 JSX 的能力通过 Vite 插件提供,这个 Vite 插件在开发环境和生产环境都会执行,因此可以得出下面这个结论:
Vite 已经将 Esbuild 的 Transformer 能力用到了生产环境。尽管如此,对于低端浏览器场景,Vite 仍然可以做到语法和 Polyfill 安全,详情见——语法降级与 Polyfill。
这部分能力用来替换原先 Babel 或者 TSC 的功能,因为无论是 Babel 还是 TSC 都有性能问题,大家对这两个工具普遍的认知都是: 慢,太慢了。
虽然 Esbuild Transfomer 能带来巨大的性能提升,但其自身也有局限性,最大的局限性就在于 TS 中的类型检查问题。这是因为 Esbuild 并没有实现 TS 的类型系统,在编译 TS (或者 TSX ) 文件时仅仅抹掉了类型相关的代码,暂时没有能力实现类型检查。
因此 vite build 之前会先执行 tsc 命令,也就是借助 TS 官方的编译器进行类型检查。
当然要解决类型问题,推荐大家使用 TS 的编辑器插件。在开发阶段就能早早把问题暴露出来并解决,不至于等到项目要打包上线的时候。
Vite 从 2.6 版本开始,就官宣默认使用 Esbuild 来进行生产环境的代码压缩,包括 JS 代码和 CSS 代码。在生产环境中 Esbuild 压缩器通过插件的形式融入到了 Rollup 的打包流程中
传统的方式都是使用 Terser 这种 JS 开发的压缩器来实现,在 Webpack 或者 Rollup 中作为一个 Plugin 来完成代码打包后的压缩混淆的工作。但 Terser 其实很慢,主要有 2个原因。
因此,Esbuild 这种从头到尾共享 AST 以及原生语言编写的 Minifier 在性能上能够甩开传统工具的好几十倍。
举个例子,测试实际大型库的压缩性能(数值来自 esbuild 官方基准,随版本和机器而异)。压缩一个大小为 3.2 MB 的库,Terser 需要耗费 8798 ms ,而 Esbuild 仅仅需要 361ms ,压缩效率较 Terser 提升了二三十倍,并且产物的体积几乎没有劣化,因此 Vite 果断将其内置为默认的压缩方案。
总的来说,Vite 将 Esbuild 作为自己的性能利器,将 Esbuild 各个垂直方向的能力( Bundler 、 Transformer 、 Minifier )利用的淋漓尽致,给 Vite 的高性能提供了有利的保证
Rollup 在 Vite 中的重要性一点也不亚于 Esbuild,它既是 Vite 用作生产环境打包的核心工具,也直接决定了 Vite 插件机制的设计
虽然 ESM 已经得到众多浏览器的原生支持,但生产环境做到完全 no-bundle 也不行,会有网络性能问题。为了在生产环境中也能取得优秀的产物性能,Vite 默认选择在生产环境中利用 Rollup 打包,并基于 Rollup 本身成熟的打包能力进行扩展和优化,主要包含3 个方面:
CSS 代码分割。如果某个异步模块中引入了一些 CSS 代码,Vite 就会自动将这些 CSS 抽取出来生成单独的文件,提高线上产物的缓存复用率
自动预加载。Vite 会自动为入口 chunk 的依赖自动生成预加载标签 <link rel="modulepreload"> ,这种适当预加载的做法会让浏览器提前下载好资源,优化页面性能
<head>
<!-- 省略其它内容 -->
<!-- 入口 chunk -->
<script type="module" crossorigin src="/assets/index.250e0340.js"></script>
<!-- 自动预加载入口 chunk 所依赖的 chunk-->
<link rel="modulepreload" href="/assets/vendor.293dca09.js">
</head>无论是开发阶段还是生产环境,Vite 都根植于 Rollup 的插件机制和生态,如下面的架构图所示:在开发阶段,Vite 借鉴了 WMR 的思路,自己实现了一个 Plugin Container ,用来模拟 Rollup 调度各个 Vite 插件的执行逻辑,而 Vite 的插件写法完全兼容 Rollup,因此在生产环境中将所有的 Vite 插件传入 Rollup 也没有问题。
反过来说,Rollup 插件却不一定能完全兼容 Vite。不过,目前仍然有不少 Rollup 插件可以直接复用到 Vite 中,你可以通过这个站点查看所有兼容 Vite 的 Rollup 插件: https://vite-rollup-plugins.patak.dev/
而 Vite 的做法是从头到尾根植于的 Rollup 的生态,设计了和 Rollup 非常吻合的插件机制,而 Rollup 作为一个非常成熟的打包方案,从诞生至今已经迭代了 六年多 的时间,npm 年下载量达到 上亿次 ,产物质量和稳定性都经历过大规模的验证。某种程度上说,这种根植于已有成熟工具的思路也能打消或者降低用户内心的疑虑,更有利于工具的推广和发展