本文正在参加「金石计划 . 分割6万现金大奖」

一次偶尔的时机,我将项目(依据 tdesign-vue-next-starter )由 Vite 2.7 晋级成 Vite 3.x 后,发现初次运转 Vite dev 构建,页面首屏时刻非常长,且一定会整个页面改写一次。而第2次进入则不再改写页面。

充溢好奇心的我,决议研讨一下为什么 Vite.3.x 会有这么一个负优化,于是我细心研讨源码,最终发现了问题的本源,并给 Vite 提交了修正的代码

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

大约测了一下,修正前的页面首屏时刻为 1m06s,修正后为 45s,功能提升了 25%

问题详情

晋级 Vite3.x 后的代码放到了该库房,感兴趣的同学能够自行调试

项目晋级 Vite3.x 后,初次进入页面,页面的首屏时刻非常的长,且一定会改写整个页面,这个问题只要在没有 Vite 缓存情况下呈现。

由于咱们能够通过以下方式复现:

vite --force

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

从日志中,能够初步判断出,Vite 在运转进程中,发现了新的依靠,然后从头履行预构建,再改写页面。

因而咱们需求更多的信息,要打印更多的运转 log,以清楚 Vite 的运转状态。这儿咱们能够通过设置 DEBUG 环境变量,来输出更多的关于依靠构建相关的日志:

# vite:deps 是指过滤出依靠预构建的日志
# force 代表不运用之前构建的缓存,以确保每次都能复现问题
cross-env DEBUG=vite:deps vite --force

运转结果如下:

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

咱们来细心看一下日志信息:

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

只是从日志的字面意思,咱们能够得出以下信息:

  1. Dev server 发动
  2. 依靠扫描,扫描出了项目中运用了哪些依靠。这儿扫描到的依靠是不全的
  3. 拜访页面后,发现新的依靠(lodash/union),从头履行依靠构建
  4. 发现新的依靠(echarts/charts、echarts/renderer 等),又从头履行依靠构建
  5. 改写页面

看起来便是由于依靠扫描的时候,有许多依靠没有被扫描出来,那么这些依靠没有被预构建。导致运转代码时,屡次发现新的依靠(没有进行预构建),导致又要从头履行预构建,最后还改写了页面。

因而可能问题的本源是:Vite 的依靠扫描没有扫描到一切的依靠

Vite 的依靠扫描

这块涉及到 Vite 依靠扫描的相关常识,恰好之前就研讨过这个内容,还写了一篇文章:《五千字深度解读 Vite 的依靠扫描》,这儿总结一下:

  1. 用 esbuild 打包一遍整个项目
  2. 打包进程中遇到 import 句子,就把 import 的内容记载下来,例如 import Vue,就记载 Vue 到数组中
  3. 最后只留下实践路径为 node_module 中的依靠,这些代码便是第三方依靠。

假如有如下的模块依靠树,则扫描到的依靠便是 vueaxios

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

模块依靠树是利用模块中的 import 句子(静态 import、动态 import 均可),将各个模块连接起来的。

Vite 文档也同时指出,Vite 默认的依靠发现为启发式,可能并不总是可取

什么时候 Vite 的依靠发现不可靠呢?

当源代码中没有 import 句子,但经过代码编译转化后才有 import 句子,这种情况,Vite 无法依靠扫描。只能在浏览器恳求模块,Vite 转化后,在运转时发现新依靠

提出和验证猜测

咱们看看项目中的模块依靠树(节选):

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

router.ts 的部分代码如下:

// 主动导入modules文件夹下一切ts文件
// glob 和 globEager 效果相同,只是转化后,是动态引进仍是静态引进的差异
const modules = import.meta.globEager('./modules/**/*.ts');

这是一种很常见的用法,一切的 vue-router 装备写到 modules 文件夹下,然后 router.ts 主动引进该文件的一切模块,然后传给 vue-router。

整个项目中,除了 router.ts 中运用 glob 特性进行引进模块外,其他模块均运用静态 import 或动态 import 句子引进模块。因而依靠扫描流程中,唯一可能呈现问题的,便是在依靠扫描阶段 glob 没有进行转化

要想验证 Vite3.x 在依靠扫描阶段没有转化 glob,只需求在 Vite2.x 中找到转化代码,而在 Vite3.x 中找不到即可。

经过考证,我从这个 pull request 中得知,Vite3.x 重构了 import.meta.glob 的转化,但却删去对 JS 代码中 glob 的转化,然后导致依靠扫描不全。

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

知道问题之后,咱们只要将 glob 的转化逻辑加上即可

如何修正,这个进程就不细说了,由于也不需求关心了,说多了反而让文章更难了解。

为了进一步了解 Vite 的运转机制,咱们研讨一下这个问题:

为什么依靠扫描不全,会导致后面的一系列问题(依靠从头构建、页面改写)

依靠扫描不全后的运转进程

咱们需求对照运转日志和模块依靠树,来解析依靠扫描不全后的 Vite 的整个运转进程:

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%
我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

  1. import.meta.glob 没有被转化,Vite 认为 router.ts 下只要 Login.vue,Login.vue 下的依靠被 Vite 发现,但 base.ts 等模块及其嵌套运用的依靠,并没有被扫描到
  2. 第一次依靠预构建完结
  3. 拜访页面,履行时,恳求 router.ts 页面,router.ts 被 Vite 转化
  4. 浏览器履行 router.ts 代码,动态 import base.ts,在浏览器运转时才知道有 base.ts 模块
  5. 恳求 base.ts,Vite 转化 base.ts 并回来
  6. 履行 base.ts 代码,恳求静态 import Layout.vue ,Vite 发现新依靠 echarts/charts 等, 从头履行依靠预构建
  7. 第2次依靠预构建完结
  8. 浏览器履行 base.ts 的代码,发现有动态 import dashboard.vue 模块
  9. 恳求 dashboard.vue 及其嵌套的模块,发现新依靠 echart/charts,从头履行依靠预构建
  10. 第三次依靠预构建完结

以下是这一进程的图示,从第 3 点开始画的

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

静态 import 和动态 import 的差异?

静态 import:堵塞代码履行,有必要要等 import 的模块加载完结,才会履行当时模块的代码

动态 import:异步加载模块,不堵塞当时模块代码履行。

咱们来看下面这个片段。

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

base.ts 是静态 import Layout.vue 的,因而 base.ts 有必要要等它嵌套的依靠加载完结,才会履行。但由于嵌套的 SiderNav 依靠了 lodash/unionlodash/union 又有必要等构建完结,才能回来。

因而 base.ts、Layout.vue、SiderNav.vue 三个模块都被堵塞了。

再来看这个片段:

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

当 base.ts 代码运转时,才发现有动态的 import dashboard.vue,在恳求 dashboard.vue 进程中,又发现了新的依靠 echart/charts,又需求从头预构建。

结合这两个片段,咱们会发现这两次发现新依靠,并没有方法组成一次构建,即便 Vite 有延迟履行从头构建的才能

由于发现新依靠 lodash/union,base.ts 是被堵塞的,无法履行代码,这就无法知道需求恳求 dashboard.vue,也就无法知道有新的依靠 echart/charts

这便是依靠扫描不全导致的严重后果:由于静态 import 堵塞代码履行,导致运转进程中屡次发现新依靠,屡次从头预构建。

因而这次的修正,其实对功能提升远远大于 25%,原因有以下两点:

  1. 运转进程中还会发现新的依靠,导致从头预构建
  2. 依靠扫描完好后,扫描出非常多的依靠,一切的这些依靠构建时刻为 40s;而没修正前,只是扫描出少量的依靠,构建时刻只是不到 10s。两者构建的依靠数量本身就相差较大的。

每次发现新的依靠,有必要从头构建吗?

有必要从头构建

官方文档提到了, Vite 构建的两个目的:

  1. CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将一切代码视为原生 ES 模块。因而,Vite 有必要先将作为 CommonJS 或 UMD 发布的依靠项转化为 ESM。
  2. 功能: Vite 将有许多内部模块的 ESM 依靠关系转化为单个模块,以进步后续页面加载功能。

因而新的依靠,有必要要等构建完结才能回来,期间会造成堵塞

为什么只要最后一次依靠预构建才会改写页面?

咱们来看看三次构建的产品(节选):

我修正了一个 Vite Bug,让我的项目首屏功能进步了 25%

  1. 第一次构建,有 echart/corelodash/keys
  2. 第2次构建,新发现了 lodash/union,该依靠跟原有依靠,没有任何公共代码,因而打包的产品也不会相互依靠
  3. 第三次构建,新发现了 echart/charts,它与 echart/core 有公共的依靠,打包产品会多了一份公共的代码,它们都依靠这份公共代码。

第三次构建与第2次构建比照, echart/core 的模块文件已经被改变(原来自己一切代码都在一个模块,现在公共代码被抽离),原先浏览器拉取的 echart.core 代码已经是失效的代码,这时候只能改写页面,让浏览器从头拉取最新的 echart/core

Vite 实践上会依据打包前后的 file hash,来决议是否需求改写页面,假如一切依靠的构建前后文件 hash 没有被改变,则不会改写页面,例如第2次构建,只新增了 lodash/union,其他模块没有被改变。

总结

文章就写到这了,第一次给 Vite 奉献代码,的确有点小激动。虽然是一个小小的 bug,但实践上进程是充溢坎坷的,每一个小小的问题都能研讨几天,但最后回顾起来,这个进程学到了许多收获仍是非常大的。

假如这篇文章对您有所帮助,能够点赞加收藏,您的鼓励是我创作路上的最大的动力。也能够重视我的公众号订阅后续的文章:Candy 的修仙秘籍(点击可跳转)

相关阅览

  • 《五千字深度解读 Vite 的依靠扫描》
  • 《快速了解 Vite 的依靠预构建》

更多内容能够检查我的专栏:《Vite 规划与实现》