性能利器:Esbuild
必须要承认的是,Esbuild的确是 Vite 高性能的得力助手,在很多关键的构建阶段让 Vite 获得了相当优异的性能,如果这些阶段用传统的打包器/编译器来完成的话,开发体验要下降一大截。
一、依赖预构建——作为Bundle工具
首先是开发阶段的依赖预构建阶段。
一般来说,node_modules
依赖的大小动辄几百 MB 甚至上 GB ,会远超项目源代码,相信大家都深有体会。如果这些依赖直接在 Vite 中使用,会出现一系列的问题,这些问题我们在依赖预构建的小节已经详细分析过,主要是 ESM 格式的兼容性问题和海量请求的问题,不再赘述。总而言之,对于第三方依赖,需要在应用启动前进行打包并且转换为 ESM 格式。
当然,Esbuild 作为打包工具也有一些缺点.
- 不支持降级到ES5的代码。这意味着在低端浏览器代码会跑不起来。
- 不支持const enum等语法。这意味着单独使用这些语法在esbuild中会直接抛出错误。
- 不提供操作打包产物的接口,像 Rollup 中灵活处理打包产物的能力(如renderChunk钩子)在 Esbuild 当中完全没有。
- 不支持自定义
Code Splitting
策略。传统的 Webpack 和 Rollup 都提供了自定义拆包策略的 API,而 Esbuild 并未提供,从而降低了拆包优化的灵活性。
尽管 Esbuild 作为一个社区新兴的明星项目,有如此多的局限性,但依然不妨碍 Vite 在开发阶段使用它成功启动项目并获得极致的性能提升,生产环境处于稳定性考虑当然是采用功能更加丰富、生态更加成熟的 Rollup 作为依赖打包工具了。
二、单文件编译——作为TS和JSX编译工具
在依赖预构建阶段, Esbuild
作为 Bundler
的角色存在。而在 TS(X)/JS(X)
单文件编译上面,Vite 也使用 Esbuild 进行语法转译,也就是将 Esbuild 作为 Transformer
来用。
也就是说,Esbuild 转译 TS 或者 JSX 的能力通过 Vite 插件提供,这个 Vite 插件在开发环境和生产环境都会执行,因此,我们可以得出下面这个结论:
Vite 已经将 Esbuild 的 Transformer
能力用到了生产环境。
这部分能力用来替换原先 Babel
或者 TSC
的功能,因为无论是 Babel 还是 TSC都有性能问题,大家对这两个工具普遍的认知都是: 慢,太慢了。
可以看到,虽然 Esbuild Transfomer
能带来巨大的性能提升,但其自身也有局限性,最大的局限性就在于 TS 中的类型检查问题。这是因为 Esbuild 并没有实现 TS 的类型系统,在编译 TS(或者 TSX) 文件时仅仅抹掉了类型相关的代码,暂时没有能力实现类型检查。
也因此,快速上手这一节,我让大家注意初始化工程的构建脚本,vite build
之前会先执行tsc
命令,也就是借助 TS 官方的编译器进行类型检查。
当然,要解决类型问题,我更推荐大家使用 TS 的编辑器插件。在开发阶段就能早早把问题暴露出来并解决,不至于等到项目要打包上线的时候。
三、代码压缩——作为压缩工具
Vite 从 2.6 版本开始,就官宣默认使用 Esbuild 来进行生产环境的代码压缩,包括 JS 代码和 CSS 代码。
那为什么 Vite 要将 Esbuild 作为生产环境下默认的压缩工具呢?因为压缩效率实在太高了!
传统的方式都是使用 Terser
这种 JS 开发的压缩器来实现,在 Webpack 或者 Rollup 中作为一个 Plugin 来完成代码打包后的压缩混淆的工作。但 Terser 其实很慢,主要有 2 个原因。
- 压缩这项工作涉及大量 AST 操作,并且在传统的构建流程中,AST 在各个工具之间无法共享,比如 Terser 就无法与 Babel 共享同一个 AST,造成了很多重复解析的过程。
- JS 本身属于解释性 + JIT(即时编译) 的语言,对于压缩这种** CPU 密集型**的工作,其性能远远比不上 Golang 这种原生语言。
因此,Esbuild 这种从头到尾共享 AST 以及原生语言编写的 Minifier 在性能上能够甩开传统工具的好几十倍。
总的来说,Vite 将 Esbuild 作为自己的性能利器,将 Esbuild 各个垂直方向的能力(
Bundler、Transformer、Minifier
)利用的淋漓尽致,给 Vite 的高性能提供了有利的保证。
构建基石:Rollup
Rollup 在 Vite 中的重要性一点也不亚于 Esbuild,它既是 Vite 用作生产环境打包的核心工具,也直接决定了 Vite 插件机制的设计。
一、生产环境Bundle
虽然 ESM 已经得到众多浏览器的原生支持,但生产环境做到完全no-bundle
也不行,会有网络性能问题。为了在生产环境中也能取得优秀的产物性能,Vite 默认选择在生产环境中利用 Rollup 打包,并基于 Rollup 本身成熟的打包能力进行扩展和优化,主要包含 3 个方面:
- CSS代码分割。如果某个异步模块中引入了一些CSS代码,Vite就会自动将这些CSS抽取出来生成单独的文件,提高线上产物的缓存复用率。
- 自动预加载。Vite会自动为入口chunk的依赖自动生成预加载标签
<link rel="modulepreload">
.这种适当预加载的做法会让浏览器提前下载好资源,优化页面性能 - 异步 Chunk 加载优化。在异步引入的 Chunk 中,通常会有一些公用的模块,如现有两个异步引入的
Chunk
:A
和B
,而且两者有一个公共依赖C
,一般情况下,Rollup 打包之后,会先请求A
,然后浏览器在加载A
的过程中才决定请求和加载C
,但 Vite 进行优化之后,请求A
的同时会自动预加载C
,通过优化 Rollup 产物依赖加载方式节省了不必要的网络开销。
二、兼容插件机制
无论是开发阶段还是生产环境,Vite 都根植于 Rollup 的插件机制和生态.
在开发阶段,Vite 借鉴了 WMR 的思路,自己实现了一个 Plugin Container
,用来模拟 Rollup 调度各个 Vite 插件的执行逻辑,而 Vite 的插件写法完全兼容 Rollup,因此在生产环境中将所有的 Vite 插件传入 Rollup 也没有问题。