Vite 3.0 对依赖预构建(Dependency Pre-bundling)的内部机制和应对策略

7 阅读3分钟

为什么要预构建

Vite 的核心逻辑是利用浏览器原生的 ESM 导入(crawling of static imports)。

但是在冷启动时,如果直接让浏览器去加载像 lodash-es 这种包含成百上千个小模块的第三方库,网络瀑布流会直接把浏览器卡死。

因此,我们必须在本地用 esbuild 提前把它们打成一个或几个大包,也就是所谓的预构建(optimize)。

标准的工作流

  • scan 阶段 :当你在命令行输入 vite 启动开发服务器时,Vite 会先执行一次快速的扫描。我们用 esbuild 去解析项目的入口文件,找出里面所有静态声明的第三方依赖(dependencies)。

  • optimize 阶段 :扫描出来有哪些依赖之后,紧接着用 esbuild 把它们打包好,缓存在 node_modules/.vite 里。

  • crawling of static imports :这时候浏览器开始顺着静态引用的主干网络去爬取、加载文件。

  • send optimized deps to the browser :因为所有依赖已经在第 2 步处理完毕,当浏览器请求这些第三方库时,Vite 可以直接把预构建好的、处理过的缓存文件立刻塞给浏览器。整个过程极其丝滑。

扫描遗漏(The scanner missed some deps)

但很多时候,有些依赖并不是老老实实写在最顶层的静态 import 里的。比如你用了一个第三方组件库,它内部在某个特定路由或深层组件里才动态或者隐式地引入了另一个包;或者是在一些动态生成的代码里包含的隐式依赖。这时候,第一步的 scan 就漏掉了。

  • missing dep 发现 :前期扫描和第一次优化都以为完事了。浏览器顺着黑色的长箭头一路往下爬取和解析代码。爬着爬着,突然在某个深层文件里发现:“卧槽,这里怎么突然冒出一个之前没登记过的依赖?”

  • 连锁反应:当发现这个 missing dep 时,旧的预构建产物可能已经失效了(因为新老依赖之间可能有拓扑关系或者需要合流)。

  • 二次优化(optimize :Vite 必须紧急刹车。在 3.0 里,它会把这个新依赖加进队列,然后重新触发一次 esbuild 预构建

  • 延迟交付 :这就导致浏览器必须卡在这里,等 Vite 把这一轮新的优化跑完。最后,才能把这批最新、最全的预构建依赖发送给浏览器。

补充

在 2.0 时期这种情况甚至会导致浏览器直接发生一次全页面的硬刷新(Full Page Reload) ,用户体验非常割裂。

在 3.0 时期对这一块做了大量的重构和优化。引入了一个暂存机制(staged pre-bundling)。当发现新依赖时,Vite 会在后台悄悄进行新一轮的打包,而尽量不直接去干掉当前已经发给浏览器的旧文件。等新一轮打包彻底稳妥了,再精准地让浏览器去请求新的模块,或者最大程度地减少页面重载的代价,从而在不破坏开发节奏的前提下,把这个“漏网之鱼”给平稳地补上。