Vite 4.3发布!15+优化策略带来1.5x ~2x 性能提升!
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
今天给大家带来的主题是 Vite 4.3版本的发布,总体性能提升大约 1.5 ~2倍,同时带着大家一起来聊聊 Vite 4.3的 10 大优化手段。深入后面内容阅读之前非常建议大家提前阅读相关专题,以下是已发表文章的传送门:
- 《 Vite大火!Snowpack 如何!》
- 《 Snowpack 摸鱼!Vite 接管! 》
- 《 Vite 反复提到 WMR 是何方神圣 ! 》
- 《 Vite 成功背后的人!Rollup! 》
- 《 Vite 优于Create-React-App (CRA) 的4个原因! 》
- 《 Vite/snowpack如何用 esbuild 提升 100x 构建速度!》
- 《 是时候考虑用SWC替换Terser了!》
话不多说,直接开始。
1.Vite 生态的四大金刚
1.1 ESM
ESM 代表 EMACScript 模块。 它是 JavaScript 语言规范的最新补充,用于处理如何在 Javascript 应用程序中加载模块。在 ES6 之前,JS 不支持模块化,对于大型项目的开发很不友好,所以社区出现了 CommonJS 、 AMD 、CMD、UMD等不同的模块规范,但在 ES6 中引入 ESM 之后,JS 从此有了自己的模块体系,CommonJS/CMD/AMD/UMD 也不再需要。
值得注意的是,Chrome >61、Safari > 11、Firefox > 60、Edge > 16、Opera > 48 等主流浏览器都已经原生支持 ESM,ESM在短短的几年经历了非常快速的发展。
1.2 Rollup
Rollup 是 JavaScript 的模块打包器,它可以将小块代码编译成更大更复杂的东西,例如库或应用程序。
Rollup 使用标准化的 ES 模块来编写代码,而不是 CommonJS 和 AMD 等特殊模块解决方案,因为 ES 模块让开发者可以自由、无缝地组合最喜欢的库中最有用的函数。 而 Rollup 负责优化 ES 模块以在现代浏览器中更快地进行本机加载。
对于支持 ES 模块的环境,Rollup 可以输出优化的 ES 模块; 对于不需要 ES 模块的环境,Rollup 可以将代码编译为其他格式,例如 CommonJS 、AMD 和 IIFE 风格的脚本,从而使得最大程度上面向未来编码。
Rollup具有以下明显优势:
- 支持多种输出格式:ES 模块、CommonJS、UMD、SystemJS 等, 打包产物不仅适用于Web,也适用于许多其他平台,比如Node。
- Tree-shaking,也称为“Live Code Inclusion”,是 Rollup 消除给定项目未使用的代码的过程。 它是一种消除死代码的形式,但在输出大小方面比其他方法更有效。
- 仅使用输出格式的导入机制而不是客户加载程序代码,根据不同的入口点和动态导入拆分代码
- Rollup 有一个易于学习的插件 API,允许开发者使用很少的代码实现强大的代码注入和转换, 已经被 Vite 和 WMR 采用。
1.3 Esbuild
esbuild 是一个用 Go 语言编写的用于打包,压缩 Javascript 代码的工具库。它最突出的特点就是打包速度极快 (extremely fast),下图是 esbuild 跟 webpack, rollup, Parcel 等打包工具打包效率的一个 benchmark。
esbuild具有以下明显的优势:
- 用 Go 编写并编译为本机代码,而Go 的设计核心是并行性
- 内部的算法经过精心设计,使得所有可用的 CPU 内核完全饱和。
- 自己编写所有内容而不是使用三方库,从而带来了很多性能优势。
- 最大限度重用AST、存储内存等
1.4 SWC
SWC(Speedy Web Compiler) 是一个可扩展的基于 Rust 的平台,用于下一代快速开发工具。 Next.js、Parcel 和 Deno 等工具以及 Vercel、字节跳动、腾讯、Shopify 等公司都在使用 SWC。
在 Github 上,SWC 已经有超过 26.3k 的 star 和 1k 的 fork,每周的平均下载量达到了 1983k。目前有超过 6.2k 的项目使用 SWC、项目贡献人数也达到了 200+,增长势头非常迅猛。
SWC 可用于编译和打包工作,是一个超快速的 JavaScript 编译器。 对于编译,SWC 读取 JavaScript / TypeScript 文件,并输出所有主流浏览器都支持的代码。在性能上,SWC 比其他打包方案具有明显的优势。
在性能优势上,因为有Rust的加持,SWC官方宣称: 在单线程上比 Babel 快 20 倍,在四核上快 70 倍。
同时,SWC 还被设计为可扩展的。目前,支持以下核心能力,更多能力也在不断增强:
- 代码编译(Compilation)
- 打包:swcpack,开发中
- 代码压缩
- 使用 WebAssembly进行转换
- Webpack集成:使用swc-loader
- 提高Jest性能:使用 @swc/jest
- 自定义插件
Vite 4.x 开始借助了 SWC 的诸多能力,特别是在 React 项目中,可以利用@vitejs/plugin-react、@vitejs/plugin-react-swc (new)插件。这也是为什么下面性能对比的图中有babel、swc两种方案的原因。
上面介绍了Vite生态的几个主角,接下来大家一起来看看什么是Vite。
2.什么是 Vite
Vite 是由 Vue.js 的创建者 Evan You 推出的下一代前端构建工具。 它是官方 Vue CLI 的替代品,速度非常快。Vite利用原生 ESM 并使用 Rollup 处理开发和打包工作。 从功能上讲,它的工作方式类似于预配置的 webpack 和 webpack-dev-server,但在速度方面具有无可比拟的优势。
Vite 具有以下明显特征:
- Vite 提供了一个 HMR API, 在应用程序运行时交换、添加或删除模块,无需完全重新加载。 显著加快开发过程,因为在对应用程序进行更改时会保留应用程序状态。 如果对任何文件进行更改,会注意到更改的速度比普通 Vue 或 React 应用程序快得多。
- Vite 支持开箱即用的 .ts 文件, 它使用名为 esbuild 的极其快速的 JavaScript 打包器将 TypeScript 代码转换为 JavaScript。
- Vite 在提供的所有 JavaScript 文件中检测类裸模块导入,并将它们重写为解析路径以反映包在 node_modules 文件夹中的位置,以便浏览器可以正确处理它们。
- Vite 开箱即用地支持 Vue 3 单文件组件 、Vue 3 JSX 组件和 Vue 2 组件。 它还支持 JSX 文件(.jsx、.tsx)、CSS 文件导入、PostCSS、CSS 模块和 CSS 预处理器,如 Sass、LESS 和 Stylus。
3. Vite 4.3 新特性
3.1 性能改进明显
在这个 minor 发布中,Vite 专注于提高开发服务器的性能。 解析逻辑得到简化,改进了热更新路径并实现了更智能的缓存以查找 package.json、TS 配置文件和解析 URL。
与 Vite 4.2 相比,Vite 4.3 带来了全面的速度提升。
下面是测量的性能改进,它测试了具有 1000 个 React 组件的应用冷热启动场景下,开发服务器的时间开销,以及根组件和叶组件的 HMR 时间。
此性能运行的规格和版本如下:
- CPU:Ryzen 9 5900X,内存:DDR4-3600 32GB,SSD:WD Blue SN550 NVME SSD
- Windows 10 专业版 21H2 19044.2846
- Node.js 18.16.0
- Vite 和 React 插件版本
- Vite 4.2 (babel): Vite 4.2.1 + plugin-react 3.1.0
- Vite 4.3 (babel): Vite 4.3.0 + plugin-react 4.0.0-beta.1
- Vite 4.2 (swc): Vite 4.2.1 + plugin-react-swc 3.2.0
- Vite 4.3 (swc): Vite 4.3.0 + plugin-react-swc 3.3.0
总体来看,早期报告表明,在测试 Vite 4.3 测试版时,实际应用程序的开发启动时间提高了 1.5 ~ 2 倍,接下来一起看看Vite做了哪些性能优化策略。
3.2 详述15大性能改进手段
更智能的 resolve 策略
Vite 解析所有接收到的 URL 和路径以获取目标模块。
在 Vite 4.2 中,有很多冗余的 resolve 逻辑和不必要的模块搜索。 Vite 4.3 使 resolve 逻辑更简单、更严格、更准确,以减少计算和 fs 调用。
更简单的解决方案
Vite 4.2严重依赖 resolve 包来解析依赖的 package.json,查看 resolve 的源码发现解析 package.json 时有很多无用的逻辑。
Vite 4.3 摒弃了此 resolve,遵循更简单的 resolve 逻辑:直接检查嵌套父目录中是否存在 package.json。
更严格的 resolve
Vite 必须调用 Nodejs fs API 来查找模块, 但是 IO 很昂贵。
Vite 4.3 缩小了文件搜索范围,并跳过搜索一些特殊路径,以尽可能减少 fs 调用。 例如:
- 由于 # 符号不会出现在 URL 中,用户可以控制源文件路径中没有 # 符号,因此 Vite 4.3 不再检查用户源文件中带有 # 符号的路径,而是仅在 node_modules 中搜索。
- 在Unix系统中,Vite 4.2会先检查根目录下的每一个绝对路径,对大多数路径都可以,但是如果绝对路径以根开头就很容易失败。 为了在 /root/root 不存在的情况下跳过搜索 /root/root/path-to-file,Vite 4.3 会在开头判断 /root/root 作为目录是否存在,并预先缓存结果。
- 当 Vite 服务器收到@fs/xxx 和@vite/xxx 时,就不需要再解析这些 URL。 Vite 4.3 直接返回之前缓存的结果,不再重新解析。
更准确的resolve
Vite 4.2 在文件路径为目录时递归解析模块,会导致不必要的重复计算。 Vite 4.3 将递归解析扁平化,并对不同类型的路径应用适当的解析,展平后缓存一些 fs 调用也更容易。
使用绝对文件路径缓存
Vite 4.3 打破了解析 node_modules 包数据的性能瓶颈。
Vite 4.3 不仅使用了绝对路径(/root/node_modules/pkg/foo/bar.js & /root/node_modules/pkg/foo/baz.js),还使用了遍历目录(/root/node_modules/pkg/foo & /root/node_modules/pkg) 作为 pkg 缓存的键。
另一种情况是,Vite 4.2 在单个函数中查找深层导入路径的 package.json。例如 Vite 4.2 解析 a/b/c/d 等文件路径时,首先检查根 a/package.json 是否存在, 如果没有,则按照a/b/c/package.json -> a/b/package.json的顺序查找最近的package.json,但事实是查找根package.json和最近的package.json应该处理分开,因为在不同的解析上下文中需要它们。 Vite 4.3 将根 package.json 和最近的 package.json 解析分成两部分,这样它们就不会混在一起。
修复 fs.realpathSync 问题
Nodejs 中有一个有趣的 realpathSync 问题,它指出 fs.realpathSync 比 fs.realpathSync.native 慢 70 倍。
export function getRealPath(path: string) { try { return realpathSync.native(path) } catch (e) { if (e.code === "ENOENT") { return null } throw e }}
但 Vite 4.2 仅在非 Windows 系统上使用 fs.realpathSync.native,因为它在 Windows 上的行为不同。 为了解决这个问题,Vite 4.3 在 Windows 上调用 fs.realpathSync.native 时添加了网络驱动器验证(network drive validation)。
HMR 防抖
考虑两个简单的依赖链 C <- B <- A & D <- B <- A,当 A 被编辑时,HMR 将从 A 传播到 C 和 A 传播到 D。这导致 A 和 B 在 Vite 中被更新两次。
Vite 4.3 缓存了这些遍历的模块,避免多次搜索。 这可能会对那些带有组件桶导入的文件结构产生很大的影响。 它也适用于由 git checkout 触发的 HMR。
并行化
并行化始终是获得更好性能的好选择。 在 Vite 4.3 中并行化了一些核心功能,包括:导入分析、提取 deps 的导出、解析模块 url 和运行批量优化器。
并行化后 Vite 4.3 确实有显著的性能改进。
非阻塞 tsconfig 解析
Vite 服务器在预绑定 ts 或 tsx 时需要 tsconfig 数据。
Vite 4.2 在服务端启动之前,需要在插件钩子 configResolved 中等待 tsconfig 数据解析完成。 而 Vite 4.3 会在服务器启动前初始化 tsconfig 解析,但服务器不会等待。 解析过程在后台运行。 但是,一旦有ts相关的请求进来,就得等tsconfig解析完了。
非阻塞文件处理
Vite 中有大量的 fs 调用,其中一些是同步的, 这些同步 fs 调用可能会阻塞主线程,Vite 4.3 将它们改为异步。
import { unlinkSync } from 'node:fs';try { unlinkSync('/tmp/hello'); // 注意:有sync标记的都是同步的方法 console.log('successfully deleted /tmp/hello');} catch (err) { // handle the error}
此外,并行化异步函数也更容易,关于异步函数需要关心的一件事是,可能有许多 Promise 对象在解析后要释放。 由于更智能的解析策略,释放 fs-Promise 对象的成本要低得多。
用回调替换 *yield
Vite 使用 tsconfck(@dominikg)来查找和解析 tsconfig 文件。 tsconfck 过去常常通过 *yield 遍历目标目录,生成器的一个缺点是它需要更多的内存空间来存储它的生成器对象,并且在运行时会有大量的生成器上下文切换。
所以Vite目前 开始在核心中用回调替换 *yield。
将 startsWith 和 endsWith 替换为 ===
Vite 4.2 使用 startsWith 和 endsWith 来检查热门 URL 中的标题和尾随 '/'。 通过 str.startsWith('x') 和 str<0> === 'x' 的执行基准,发现 === 比 startsWith 快大约 20%。
同时,endsWith 比 === 慢大约 60%。
避免重新创建正则表达式
Vite 需要大量的正则表达式来匹配字符串,大部分都是静态的,所以只用它们的单例会好很多。Vite 4.3 提升了正则表达式,以便可以被重用。
放弃生成自定义错误
Vite 4.2 中有一些自定义错误,以实现更好的 DX(开发体验)。 这些错误可能会导致额外的计算和垃圾收集,从而降低 Vite 的速度。
在 Vite 4.3 中,放弃生成一些自定义错误(例如 package.json NOT_FOUND 错误)并直接抛出原始错误以获得更好的性能。
4.本文总结
本文主要和大家探讨 Vite 4.3 版本的发布,总体性能提升大约 1.5 ~2倍,同时带着大家一起来聊了 Vite 4.3的 10 大优化手段。因为篇幅有限,文章并没有过多展开,如果有兴趣,文末的参考资料提供了优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!
参考资料
https://vitejs.dev/blog/announcing-vite4-3.html
https://www.educative.io/answers/what-is-vitejs
https://sun0day.github.io/blog/vite/why-vite4_3-is-faster.html#fs-realpathsync-issue
https://vitejs.dev/blog/announcing-vite4.html