导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

软实力

算法

UI、组件库

Node

业务技能

针对性攻坚

AI


双引擎架构:Vite 是如何站在巨人的肩膀上实现的?

所谓的 巨人 ,指的就是 Vite 底层所深度使用的两个构建引擎—— Esbuild 和 Rollup 。

Vite 架构图

很多人对 Vite 的双引擎架构仅仅停留在 开发阶段使用 Esbuild,生产环境用 Rollup 的阶段,殊不知,Vite 真正的架构远没有这么简单。一图胜千言,这里放一张 Vite 架构图:

image.png

性能利器 - Esbuild

必须要承认的是,Esbuild 的确是 Vite 高性能的得力助手,在很多关键的构建阶段让 Vite 获得了相当优异的性能,如果这些阶段用传统的打包器/编译器来完成的话,开发体验要下降一大截。

那么,Esbuild 到底在 Vite 的构建体系中发挥了哪些作用?

一、依赖预构建——作为 Bundle 工具

首先是开发阶段的依赖预构建阶段。

image.png

一般来说, node_modules 依赖的大小动辄几百 MB 甚至上 GB ,会远超项目源代码,相信大家都深有体会。如果这些依赖直接在 Vite 中使用,会出现一系列的问题,这些问题我们在依赖预构建的小节已经详细分析过,主要是 ESM 格式的兼容性问题和海量请求的问题,不再赘述。总而言之,对于第三方依赖,需要在应用启动前进行打包并且转换为 ESM 格式。

Vite 1.x 版本中使用 Rollup 来做这件事情,但 Esbuild 的性能实在是太恐怖了,Vite 2.x 果断采用 Esbuild 来完成第三方依赖的预构建,至于性能到底有多强,大家可以參照它与传统打包工具的性能对比图:

image.png

当然,Esbuild 作为打包工具也有一些缺点。

尽管 Esbuild 作为一个社区新兴的明星项目,有如此多的局限性,但依然不妨碍 Vite 在开发阶段使用它成功启动项目并获得极致的性能提升,生产环境处于稳定性考虑当然是采用功能更加丰富、生态更加成熟的 Rollup 作为依赖打包工具了。

二、单文件编译——作为 TS 和 JSX 编译工具

在依赖预构建阶段, Esbuild 作为 Bundler 的角色存在。而在 TS(X)/JS(X) 单文件编译上面,Vite 也使用 Esbuild 进行语法转译,也就是将 Esbuild 作为 Transformer 来用。

大家可以在架构图中 Vite Plugin Pipeline 部分注意到:

image.png

也就是说,Esbuild 转译 TS 或者 JSX 的能力通过 Vite 插件提供,这个 Vite 插件在开发环境和生产环境都会执行,因此,我们可以得出下面这个结论:

Vite 已经将 Esbuild 的 Transformer 能力用到了生产环境。尽管如此,对于低端浏览器场景,Vite 仍然可以做到语法和 Polyfill 安全。

这部分能力用来替换原先 Babel 或者 TSC 的功能,因为无论是 Babel 还是 TSC都有性能问题,大家对这两个工具普遍的认知都是: 慢,太慢了。

当 Vite 使用 Esbuild 做单文件编译之后,提升可以说相当大了,我们以一个巨大的、50多 MB 的纯代码文件为例,来对比 Esbuild 、 Babel 、 TSC 包括 SWC 的编译性能:

image.png

可以看到,虽然 Esbuild Transfomer 能带来巨大的性能提升,但其自身也有局限性,最大的局限性就在于 TS 中的类型检查问题。这是因为 Esbuild 并没有实现 TS 的类型系统,在编译 TS (或者 TSX ) 文件时仅仅抹掉了类型相关的代码,暂时没有能力实现类型检查。

也因此,在初始化工程的构建脚本时也提到过, vite build 之前会先执行 tsc 命令,也就是借助 TS 官方的编译器进行类型检查。

当然,要解决类型问题,我更推荐大家使用 TS 的编辑器插件。在开发阶段就能早早把问题暴露出来并解决,不至于等到项目要打包上线的时候。

三、代码压缩——作为压缩工具

Vite 从 2.6 版本开始,就官宣默认使用 Esbuild 来进行生产环境的代码压缩,包括 JS 代码和 CSS 代码

从架构图中可以看到,在生产环境中 Esbuild 压缩器通过插件的形式融入到了 Rollup 的打包流程中:

image.png

那为什么 Vite 要将 Esbuild 作为生产环境下默认的压缩工具呢?因为压缩效率实在太高了!

传统的方式都是使用 Terser 这种 JS 开发的压缩器来实现,在 Webpack 或者 Rollup 中作为一个 Plugin 来完成代码打包后的压缩混淆的工作。但 Terser 其实很慢,主要有 2 个原因。

此,Esbuild 这种从头到尾共享 AST 以及原生语言编写的 Minifier 在性能上能够甩开传统工具的好几十倍。

举个例子,我们可以看下面这个实际大型库(echarts)的压缩性能测试项目:

image.png

压缩一个大小为 3.2 MB 的库,Terser 需要耗费 8798 ms ,而 Esbuild 仅仅需要 361 ms ,压缩效率较 Terser 提升了二三十倍,并且产物的体积几乎没有劣化,因此 Vite 果断将其内置为默认的压缩方案。

总的来说,Vite 将 Esbuild 作为自己的性能利器,将 Esbuild 各个垂直方向的能力 ( Bundler 、 Transformer 、 Minifier )利用的淋漓尽致,给 Vite 的高性能提供了有利的保证。

构建基石——Rollup

Rollup 在 Vite 中的重要性一点也不亚于 Esbuild,它既是 Vite 用作生产环境打包的核心工具,也直接决定了 Vite 插件机制的设计。那么,Vite 到底基于 Rollup 做了哪些事情?

生产环境 Bundle

虽然 ESM 已经得到众多浏览器的原生支持,但生产环境做到完全 no-bundle 也不行,会有网络性能问题。为了在生产环境中也能取得优秀的产物性能,Vite 默认选择在生产环境中利用 Rollup 打包,并基于 Rollup 本身成熟的打包能力进行扩展和优化,主要包含3 个方面:

这种适当预加载的做法会让浏览器提前下载好资源,优化页面性能。

异步 Chunk 加载优化。在异步引入的 Chunk 中,通常会有一些公用的模块,如现有两个异步引入的 Chunk: A 和 B ,而且两者有一个公共依赖 C,如下图:

image.png

一般情况下,Rollup 打包之后,会先请求 A,然后浏览器在加载 A 的过程中才决定请求和加载 C,但 Vite 进行优化之后,请求 A 的同时会自动预加载 C,通过优化 Rollup 产物依赖加载方式节省了不必要的网络开销。

兼容插件机制

无论是开发阶段还是生产环境,Vite 都根植于 Rollup 的插件机制和生态,如下面的架构图所示:

image.png

在开发阶段,Vite 借鉴了 WMR 的思路,自己实现了一个 Plugin Container,用来模拟 Rollup 调度各个 Vite 插件的执行逻辑,而 Vite 的插件写法完全兼容 Rollup,因此在生产环境中将所有的 Vite 插件传入Rollup 也没有问题。

反过来说,Rollup 插件却不一定能完全兼容 Vite(这部分我们会在插件开发小节展开来说)。不过,目前仍然有不少 Rollup 插件可以直接复用到 Vite 中,你可以通过这个站点查看所有兼容 Vite 的 Rollup 插件:vite-rollup-plugins.patak.dev/。

Vite 插件开发

一个简单的插件示例

Vite 插件与 Rollup 插件结构类似,为一个 name 和各种插件 Hook 的对象:

{
	// 插件名称
	name: 'vite-plugin-xxx',
	load(code) {
		// 钩子逻辑
	}
}

如果插件是一个 npm 包,在 package.json 中的包命名也推荐以 vite-plugin 开头

一般情况下因为要考虑到外部传参,我们不会直接写一个对象,而是实现一个返回插件对象的 工厂函数 ,如下代码所示:

// myPlugin.js
export function myVitePlugin(options) {
	console.log(options);
	return {
		name: 'vite-plugin-xxx',
		load(id) {
			// 在钩子逻辑中可以通过闭包访问外部的 options 传参
		}
	}
}

// 使用方式
// vite.config.ts
import { myVitePlugin } from "./myVitePlugin";
export default {
	plugins: [myVitePlugin({ /* 给插件传参 */ })]
}

插件 Hook 介绍

todo

Vite 热更新原理探索

背景

代码变更后查看更新的页面效果一直以来都是前端工程师的工作流程当中出现频率最高的环节

在前端界还没有大量工具与解决方案的时代,工程师们一度是通过手动/自动刷新页面的方式来解决应对这个开发环节

但随着互联网的发展,对前端产品的要求越来越高,一个项目里出现越来越多的模块,前端工程逐渐变得庞大,手动/自动刷新页面会很大程度上影响开发体验与效率

于是,HMR 诞生了