用 Vite 加速你的生产力

987 阅读7分钟

目前组里的业务组件库因为一些历史背景原因,源码和展示站点分为两个独立的项目工程维护,而市面上对于组件、组件库、工具库的源码和站点在一个仓库维护或者采用 MonoRepo 的方式开发和维护,因此在不改变项目架构的前提下(暂时不要纠结为什么不放在一起维护),我们对于效率协同和工程化方面进行了一系列的演进,下面首先介绍下我们当前面临的问题:

面临的问题

npm link

因为源码和站点维护在两个工程下,在本地开发的时候如何关联两个项目是首先面临的问题,最初我们采用了 npm link 的方式,但是因为组件库和站点均是基于 react hook 开发,因此每次进行组件迭代开发都需要经历下面几步:

  • 站点下: cd node_modules/react && npm link 和 cd node_modules/react-dom && npm link
  • 源码下: npm link react && npm link react-dom
  • 源码下: npm link
  • 站点下: npm link 源码

不难发现,这样的方案有以下痛点(每个新参与开发的同学都会吐槽 diss):

  • 操作繁琐易出错;
  • yarn 和 npm 混和使用使得 link 问题频出;
  • link 断链;

站点 & 源码,双编译

这一版我们完全抛弃了 npm link,采取了比较 hack 的方式:

  • 源码 watch 文件变更,实时编译构建 ESM;
  • 源码 watch ESM 变更,实时同步到站点 node_modules 下;
  • 站点 watch node_modules 下组件 ESM 变更,热更新;

不难发现,这样的方案有以下痛点:

  • 监听 node_modules 变更,姿势很 hack;
  • 双边 watch + 编译,严重消耗内存资源;
  • 热更新时间 = 组件 ESM 编译时间 + 同步推送时间(可以忽略不计) + 站点编译时间,10s+妥妥的;

站点单编译

既然站点和源码都具有编译构建能力,为什么不减少一次编译构建呢?为此我们进行了第三次优化升级:

  • 源码 watch 文件变更,实时同步源码到站点缓存目录下 ;
  • 开发环境下,站点 import node_modules 下 ESM 变更为 import 缓存目录下组件源码;
  • 站点 watch 缓存目录下组件源码变更,热更新; 虽然这次升级后已经解决了大部分的问题,但是还是存在问题。

源码本地开发的优化使得站点编译压力的增加:

  • 热更新慢:改个文案 hotreload 2s +;
  • 冷启动更慢:60s 妥妥的;

其实到现在这一步问题已经一定程度往 webpack 衍生方案的通病上靠了,因此,我们把目标聚焦在了新的构建工具上。

为什么选择 Vite ?

下面我列举了一常见的构建工具以及以及一些选型思考。

首先是 Webpack 以及 Webpack 衍生方案:

  • 基于 CRA 封装的业务脚手架;
  • CRA;
  • Webpack 轻量级配置;

基于 Webpack 的方案和目前使用的脚手架差异不大,收益不明显,并且从根本上也解决不了 Webpack 带来的生产力问题。

接下来是目前比较火的基于 ESM 的现代方案:

  • Snowpack
  • Vite

关于比较尤大的文章更有说服力:对比

此外,就我个人而言,我觉得尤大在华人社区以及国内的影响力更大一些,因此尤大开源的工具在国内社区活跃度会更高一些,关注度也会更高,对于后续的可持续发展也比较有利,因此在选型上我们选择了 Vite。

成果收益

  • 秒级冷启动:60s+ => 3s- (缓存生效后 300ms 左右)
  • 毫秒级热更新:2s+ => 1s- (sync + hotreload)

喜大普奔,收效显著!!!

Vite 为什么快?

首次冷启动:esbuild 预编译,生成缓存

1.png 4.png 2.png

再次冷启动:利用缓存

3.png

冷启动

首先冷启动为什么快,因为 Vite 基于原生 ESM,也就是说 Vite 将原来 Webpack 前置构建 bundle 的工作交给了性能强悍的现在浏览器来做,所以冷启动时间大幅缩减(毕竟冷启动最耗时的就是分析代码构建 bundle)。

而在 Vite 中,正如官方文档介绍的那样,Vite 将我们的代码氛围了依赖和源码两部分:

  • 依赖:大多为在开发时不会变动的纯 JavaScript。Vite 将会使用 esbuild 预构建依赖。esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍
  • 源码:通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)

bundle based dev server

以 Webpack 为代表的“bundle based dev server”,在冷启动前需要依赖诸如 babel 之类的工具对代码进行分析并编译生成可运行的 bundle,这一构建时的过程造成了冷启动时间过长。

ESM based dev server

而对于“ESM base dev server”来说,dev server 依赖的是我们 ESM,而我们的代码本身就是以 ESM 编写的,因此进去要告知 dev server 目标模块路径加载即可,编译解析交给浏览器的运行时去处理,从而大大加速了我们的冷启动。

热更新

基于原生 ESM

在 Vite 中,HMR 是在原生 ESM 上执行的。和冷启动一样的道理,当我们修改了一个文件后,不需要在重新编译了,热更新也就大大减少耗时了。

配合 http 头

Vite 充份利用了 http 缓存,这也是我们在本地开发时候看到的一个和 webpack 比较大的不同,network 中充满了大量模块请求。

  • 源码部分:304 配合 ETag 协商缓存

5.png

  • 依赖模块:强缓存

6.png

Webpack to Vite ?

那么如何平稳的从 Webpack 迁移到 Vite ? 其实,当你按照 Vite 文档落地的时候已经兼容 80%的 Webpack 场景。只需要考虑一些额外 case:

ESM 及 ESM 衍生问题

Vite 利用了 esBuild 去预构建 ESM,因此对包的要求相对严格,对于 esBuild 预编译失败的场景需要 case by case 处理。

例如:

esBuild 预编译失败

这里以 react-virtualized 为例,react-virtualized 的 WindowScroll.js 下引入了 flow 的类型文件导致预编译失败。

可以写 esBuild 插件、写 resolutions、拉包本地改。

888.png

9.png

dep-scan 依赖分析失败

以 react-infinite-scroller 为例,他在 package.json 中 ESM 读取目录指向了发包后被忽略的 src 源码目录,导致 dep-scan 插件查找模块失败。

777.png

ESNext bundle

Vite 构建过程由 esBuild 接管,而 esBuild 支持的 target 的最低版本为 es6,因此想要构建兼容性更强的 bundle 需要对 Vite 的产出二次编译或者使用官方给出的基于 SystemJS 的方案。

下面这张图是截取的 Vite 官网的兼容性一节:

loadimage.png

使用官方插件:

  • babel 转译并注册为 System.js 模块;
  • 添加 System.js 运行时;

6666.png

说在最后

向我们组件库的展示站点或者内部系统的工程项目说用了就用了,但是实际投入到对外的项目时我们也不得不考虑一些问题:

成本

单说从 Webpack 切换到 Vite 成本很低,甚至比 Webpack 的大版本升级还要简单。但是从 Webpack 到 Vite 不仅仅是构建工具的转换而是整个生态的迁移,那么对于历史项目已存在的 babel 插件、Webpack 插件等都需要等量替换,这是一个成本。此外,就像上一节说的那些,对于一些不规范的三方包我们也需要做额外的适配,这也是一个成本。

收益

从目前看,Vite 带来的工程效率收益比较高,但是就我个人而言尝试的仅是简单的展示站点,并不能严格和实际的业务工程划等号,并且企业级项目工程量和复杂度都比较大,Vite 是否能符合预期的带来收益我个人还不敢保证。

风险

最后,因为涉及到了 ESM,就算官方给出了 SystemJs 版本的兼容插件,但是实际投入对外项目的兼容性风险也还是未知的。