Vite
Vite 模块
Vite
中会将 模块 分为 两种 ,一种是 源代码,即我们编写的业务代码,一种是 第三方依赖代码,即 node_modules
中的代码。 Vite
之所以快是因为分别对这两种代码进行了不同的处理,即
- 源代码
no-bundle
- 依赖包 预构建
源码 no-bundle
利用 浏览器原生 ES
模块的支持,实现开发阶段 Dev Server
, 进行模块按需加载,不用整体打包再加载。
其中一个 import
语句即代表一个 http
请求,然后由 vite
的 Dev Servr
来接收这些请求、进行文件转译以及返回浏览器可运行代码。
通常我们的源代码一般都不直接是 js
代码,还可能是 vue
、 jsx
、 tsx
等,所以需要 Dev Server
在浏览器和源代码中间做一层转译。
依赖 预构建
其中 no-bundle
只是对 源代码 而言,对于 node_modules
, Vite
还是需要 bundle
(打包) 的,并且使用速度极快的 esbuild
来进行, esbuild
使用 GO
编写,相比于 JavaScript
编写的打包器预构建依赖快 10-100
倍。
为什么要预构建
- 转换依赖包支持
ESM
规范 - 解决
import
请求瀑布流问题 HMR
(热更新)时 利用http
缓存 来加速页面重载
转换依赖包为 ESM 规范
Vite
是基于浏览器原生 ES
模块规范实现的 Dev Server
, 无论是业务代码或者是依赖包,理应符合 ESM
规范才能正常运行。
但第三方依赖包的打包规范( Commonjs
、 UMD
、 ESM
)我们无法控制,如 react
包就没有 ESM
版本的产物,只有 Commonjs
规范的包,浏览器无法直接使用,故 Vite
需要将其转换成 ESM
格式的产物,使其在浏览器通过 <script type="module"><script>
的方式正常加载。
预构建的依赖包也会存放在 node_modules/.vite
目录下,并且 Vite
会将业务代码中的引包路径重写到该目录下。
请求瀑布流
前面我们说开发时一个 import
即代表一个请求,这就可能会出现 import
文件过多导致浏览器发出特别多的请求,导致页面加载的前几秒都处于卡顿状态,即 请求瀑布流问题。
例如在使用 lodash-es
中的 debounce
方法,这个方法会依赖很多工具函数,在使用的时候,每个 import
都会触发一次新的文件请求,因此在这种 依赖层级深 、 涉及模块数量多 的情况下,成百上千个网络请求量会导致页面一直卡顿(如 Chrome
同一个域名下只能同时支持 6
个 http
并发请求的限制)。
但在 Vite
进行 依赖预构建 之后, lodash-es
的代码合并成一个文件,大大减少了依赖包带来的请求过载问题。
HMR (热更新)时的 http 缓存优化
在 Vite
中 HMR
(热更新)是在 原生 ESM
上执行的, Vite
会充分利用了浏览器 http
强缓存 与 协商缓存 来优化模块请求加载。
- 源码模块的请求会根据
304 Not Modified
进行 协商缓存 - 依赖模块请求在会通过
Cache-Control: max-age=31536000,immutable
进行强缓存
Vite 双引擎 esbuild 和 rollup
通常我们说 Vite
打包构建时 开发阶段使用 esbuild
、 生产环境使用 rollup
,但其中实际更复杂。
esbuild
esbuild
在很多关键构建阶段发挥了重要作用,在很多关键的构建阶段( 如 依赖预编译 、 TS 语法转译 、 代码压缩 )让 Vite
获得了相当优异的性能。
依赖预构建 - 作为 Bundle 工具
依赖预构建时主要 对依赖包进行 esm
格式转换,合并依赖包的分散文件以减少请求。
并且采用 GO
编写的 esbuild
在作为打包工具时,其性能远超其他 js
编写的打包工具 10-100
倍。
但 esbuild
也有其缺点:
- 不支持降级到
es5
代码,所以低端浏览器无法运行 - 不支持
const enum
等语法 - 不提供操作打包产物接口,不如
rollup
灵活 - 不支持自定义
Code Splitting
策略,不如rollup
和webpack
等
单文件编译 - 作为 TS 和 JSX 编译工具
esbuild
在文件编译时也作为 Transformer
来使用( 生产环境 也应用了),替换了原先 Babel
和 TSC
的功能,相比之下提升巨大( SWC
的编译性能与 esbuild
相近 )。
但 Esbuild Transformer
也有其局限性,因为 esbuild
并没有实现 TS
的类型系统,在编译 TS/TSX
文件时只是抹掉了类型相关的代码,暂时没有能力实现类型检查。
所以 vite
官方建议项目中使用 TS
的编辑器插件,在开发阶段时检查类型问题。
代码压缩 - 作为压缩工具
Vite
从2.6
版本开始,就官宣默认使用Esbuild
来进行 生产环境 的 代码压缩,包括JS
代码和CSS
代码。
esbuild
压缩器通过插件形式融入到了 rollup
的打包流程中,压缩效率大大提升。
传统方式使用的是 Terser
这种 JS
开发的压缩器来实现,在 webpack
或 rollup
中作为 plugin
来完成代码打包压缩工作,但是速度却很慢,原因有 2
点:
- 压缩时涉及大量
AST
操作,并且在传统的构建流程中,AST
在各个工具之间无法共享,比如Terser
就无法与Babel
共享一个AST
, 造成很多重复解析 JS
本身属于 解释性 +JIT
语言,对于压缩这种CPU
密集型工作,其性能远比不上Golang
语言
因此 esbuild
这种从头到尾 共享 AST
的 Minifier
在性能上能够做到极速。
rollup
rollup
作为生产环境打包的核心工具,也直接决定了 Vite
插件机制的设计。
css
代码分割。如果某个异步模块中引入了一些CSS
代码,Vite
就会自动将这些CSS
抽取出来生成单独的文件,提高线上产物的缓存复用率。- 自动预加载。
Vite
会自动为入口chunk
的依赖自动生成预加载标签<link rel="modulepreload">
。 - 异步
Chunk
加载优化。如有依赖关系会发起如下entry->A->C
和entry->B->C
,一般情况下,rollup
打包之后 ,会先请求A
, 然后浏览器加载A
的过程中才决定请求和加载C
,但Vite
进行优化之后,请求A
的同时会自欧东预加载C
,通过 优化rollup
的产物依赖加载方式节省了不必要的网络开销。 - 兼容插件机制。开发和生成环境都依赖于
rollup
的插件机制和生态。
为什么 esbuild 性能极高
- 使用 Golang 开发 。直接编译成原生机器码,而不用像
js
一样先解析成字节码,然后转换成机器码,大大节省了程序运行时间 - 多核并行 。内部打包算法充分利用多核
CPU
优势,所有步骤尽可能并行,这也是得益于Go
当中多线程共享内存的优势 - 从零造轮子 。几乎没有使用任何第三方库,所有逻辑自己编写,大到
AST
解析,小到字符串操作,保证极致的代码性能 - 高效的内存利用 。
esbuild
从头到尾尽可能的复用一份AST
节点数据,而不用像JS
打包工具中频繁的解析和传递AST
数据 ( 如string->TS->JS->string
) ,造成时间和内存的大量浪费
vite 开发打包快的原因总结
至此可以做下简单总结了, vite
的打包速度快主要得益于以下几点
- 支持
tree-shaking
, 开发时按需打包 - 源码
no-bundle
, 给dev server
做一次编译,减少开发编译开销 node_modules
依赖包预构建,转换es module
,合并小文件- 合理利用缓存,源码做协商缓存,依赖包做强缓存
esbuild
承担开发环境的 依赖预构建,正式环境的ts
转译 和 压缩 工作esbuild
底层go
语言实现与多线程运行,比node
环境运行的webpack
快得多