Vite 默认拆包策略

842 阅读5分钟

背景

最近做了很多涉及vite打包的工作,有的是直接与vite打包原理相关如乘风挑战赛实现更好的分包策略,有的是间接与vite打包原理相关如公共离线包(juejin.cn/post/744078…) css inline(juejin.cn/post/744079…)

关于vite的打包原理网上有很多资源,这里主要分享笔者在做上述事情时通过翻看源码和资料整理出的知识。如果有大佬发现某些地方讲的不对欢迎指出呀~

Vite 默认拆包策略

简洁版(隐瞒了很多事)

保证主包只打入必需的部分。

在 Vite 2.8 及更早版本中,默认的策略是将 chunk 分割为 indexvendor。这对一些 SPA 来说是好的策略,但是要对所有应用场景提供一种通用解决方案是非常困难的。从 Vite 2.9 起,你可以通过在配置文件中添加 splitVendorChunkPlugin 来继续使用 “分割 Vendor Chunk” 策略(这个方法也要被弃用了,以后就用manualChunks吧):

// vite.config.js
import { splitVendorChunkPlugin } from 'vite'
export default defineConfig({
  plugins: [splitVendorChunkPlugin()],
})

现在的默认策略:

一方面 Vite 实现了自动 CSS 代码分割的能力,即实现一个 chunk 对应一个 css 文件,比如index.js对应一份index.css,而按需加载的 chunk Danamic.js也对应单独的一份Danamic.css`文件,与 JS 文件的代码分割同理,这样做也能提升 CSS 文件的缓存复用率。

另一方面, Vite 基于 Rollup 的manualChunksAPI 实现了应用拆包的策略:

  • 首页应用代码及其用到的第三方依赖都打包到 index.js 中且其它页面用到这部分依赖的话不会再打包这部分依赖。
  • 对于 Async Chunk 而言 ,动态 import 的代码会被拆分成单独的 chunk,如上述的Dynacmic组件。
  • 动态加载的二级页面都用了某个依赖,那这部分依赖会提取到一个bundle里
详细版

先正常分块:

getChunkAssignments的注释:每个模块都有自己的【依赖入口点dependent entry points】集合,【集合完全相同的】 或者 【静态入口点相同的】会被分配到同一个chunk。

比如getChunkAssignments注释中举的例子(实线为静态引入,虚线为动态引入): image-20240901165535302

这里A的入口点全集为[Y, X],B的入口点全集为[Y, X, D] , 由于其静态入口点集合相同,所以A和B会打包到同一个模块。

再优化小块数量:

getOptimizedChunks的注释:

首先明确几个概念:

  1. 如果A chunk 的副作用只有 A chunk 那么A是一个纯净的chunk(即无副作用)。

  2. 什么是【相关副作用correlated side effects】呢?答:比如A这个块被加载时,G、H、B肯定也需要在内存中,那G,H,B就是A的相关副作用。(A他们不一定是直接依赖的关系,可能是通过父亲的兄弟的儿子之类的关系)

    Example:

    • X -> ABC, Y -> ADE, A-> F, B -> D

    • Then taking dependencies into account, X -> ABCDF, Y -> ADEF

    • The intersection is ADF. So we know that when A is loaded, D and F must also be in memory. So we call ADF the correlated side effects of A. 可以看到A的相关副作用并不只是他自己的依赖。如果只取他的依赖,那A的副作用应该是AF(这就是【依赖副作用dependency side effects】)。

    • For entry chunks, dependency and correlated side effects are the same. 入口模块的【依赖副作用】就等于他的【相关副作用】。

怎么去合并两个块呢?

原则:合并块时相关副作用需要保持不变。(其实【依赖副作用】是【相关副作用】的一部分,对于整个应用来说加载关系一定是满足每个chunk的相关副作用的,或者说每个chunk的相关副作用就是根据整个应用的加载关系推导出来的)。

只要满足以下条件之一即可:

a) A的依赖副作用 是 B 相关副作用的 子集,B的依赖副作用 也是 A 相关副作用的子集。so no additional side effects are triggered for any entry

b) A 的依赖入口点是 B 的依赖入口点的子集,且A 的依赖副作用是 B 的相关副作用的子集。 (因为A 的依赖入口点是 B 的依赖入口点的子集这种情况下,只要 A 被加载,B 肯定已经会被加载。但有可能加载 B,但不需要加载 A。但你已经把A合并进B了,如果A依赖副作用不是B的相关副作用的子集,那么B相关副作用就改变了,但是B相关副作用是不能改变的)

执行流程是这样的:

首先根据b)去合并模块,然后从最小的模块开始根据a)去找到合并目标。

合并一定是好的吗?

合并是为了减少小包的数量,但是会导致吸纳了小包的这个包包含了可能暂时用不到的代码。如果不想要任何用不上的代码,那不配这个minChunkSIze去优化就是最好的。

总结:

他这个合并方案能尽可能减少加载无用代码。(minChunkSize是用户配的,所以对于用户来说有多余代码是符合预期的)。

vite与webpack默认策略区别

1、rollup是保证每个chunk里不会有重复的module,缺点是chunk会比较多。 webpack是chunk会比较少,但每个chunk间可能有重复的内容。

2、webpack确实更灵活,比如可以用maxChunks:5来控制最多产生5个chunk。