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快得多