Rollup Chunk 划分:默认算法与配置影响(源码级分析)
Overview
本文聚焦 Rollup 在默认多 chunk 路径下的 chunk 划分算法、动态入口优化 与 配置项影响范围,并基于源码给出可复现的结论与验证路径。
本文不讨论 chunk 命名与输出路径规则(如
chunkFileNames),只讨论会改变 chunk 划分结果的因素。
可复现基线(版本与范围)
- 源码基线(当前 Rollup 仓库提交):
c79e6c201d1f99e126d2e6bfb3f8c5c100ddcebf - 关键文件:
src/Bundle.tssrc/utils/chunkAssignment.ts
结论严格基于以上版本源码,不保证适用于其他版本或分支。
适用范围与默认前提
本文讨论 默认多 chunk 路径:
output.inlineDynamicImports = falseoutput.preserveModules = false- 未使用(或不影响本例的)
output.manualChunks
若启用 inlineDynamicImports / preserveModules / manualChunks,流程与结果会发生变化(见后文"配置项影响")。
核心概念(与源码一致)
- entry(入口):Rollup
input的静态入口模块。 - dynamic entry(动态入口):被动态 import 的目标模块,分块阶段会被加入
allEntries。 - dependent entries:模块可由哪些入口静态可达。
- atom(原子 chunk):具有相同 dependent entries 的模块集合。
- staticDependencyAtomsByEntry:入口静态可达的 atom 集合(BigInt bitset)。
- alreadyLoadedAtomsByEntry:入口被加载时"必然已在内存中"的 atom 集合(BigInt bitset)。
默认 chunk 划分算法(按执行顺序)
默认分块流程
EXEC1) 入口路径选择(Bundle 层分支)
Bundle.generateChunks 在默认路径下调用 getChunkAssignments。
const executableModule = inlineDynamicImports
? [{ alias: null, modules: includedModules }]
: preserveModules
? includedModules.map(module => ({ alias: null, modules: [module] }))
: getChunkAssignments(
this.graph.entryModules,
manualChunkAliasByEntry,
experimentalMinChunkSize,
this.inputOptions.onLog,
typeof manualChunks === 'function',
onlyExplicitManualChunks
);- 位置:
src/Bundle.ts:158-192
2) manualChunks 预分配
若配置 manualChunks,先生成 chunkDefinitions 并标记 modulesInManualChunks,后续默认算法会跳过这些模块。
if (isManualChunksFunctionForm && onlyExplicitManualChunks) {
(manualChunkModulesByAlias[alias] ||= []).push(entry);
} else {
addStaticDependenciesToManualChunk(
entry,
(manualChunkModulesByAlias[alias] ||= []),
modulesInManualChunks
);
}- 位置:
src/utils/chunkAssignment.ts:222-267
3) 建图与 dependent entries
遍历入口模块与静态依赖,记录每个模块的 dependent entries;动态 import 目标加入 allEntries。
getOrCreate(dependentEntriesByModule, module, getNewSet<number>).add(entryIndex);
for (const dependency of module.getDependenciesToBeIncluded()) {
if (!(dependency instanceof ExternalModule)) {
staticDependencies.add(dependency);
}
}
for (const { node: { resolution } } of module.dynamicImports) {
if (
resolution instanceof Module &&
resolution.includedDynamicImporters.length > 0 &&
!allEntriesSet.has(resolution)
) {
dynamicEntryModules.add(resolution);
allEntriesSet.add(resolution);
dynamicImportsForCurrentEntry.add(resolution);
}
}- 位置:
src/utils/chunkAssignment.ts:269-357
4) 原子分组(atom)
相同 dependent entries 的模块先合并为原子。
let chunkSignature = 0n;
for (const entryIndex of dependentEntries) {
chunkSignature |= 1n << BigInt(entryIndex);
}
(chunkModules[String(chunkSignature)] ||= {
dependentEntries: new Set(dependentEntries),
modules: []
}).modules.push(...modules);- 位置:
src/utils/chunkAssignment.ts:436-450
5) 动态入口"必然已加载原子"优化
计算动态入口在加载时必然已在内存中的 atom,并移除相应 dependent entry 标记(详见后文"证明流程")。
- 交集与迭代逻辑:
src/utils/chunkAssignment.ts:491-533 - 移除标记逻辑:
src/utils/chunkAssignment.ts:536-559
6) 重新聚类生成初始 chunk
更新 dependent entries 后重新聚类得到初始 chunk。
- 位置:
src/utils/chunkAssignment.ts:562-627
7) experimentalMinChunkSize 合并优化
若启用最小 chunk 大小阈值,会尝试合并小 chunk,遵循副作用与依赖约束。
- 位置:
src/utils/chunkAssignment.ts:740-769
详细的合并算法分析(包括贪心循环、副作用约束、循环检测等)参见 experimentalMinChunkSize 配置项。
动态入口优化的证明流程(核心逻辑)
目标:证明"对动态入口 D 计算必然已加载原子并移除依赖标记"是正确且安全的。
1) 动态入口的动态依赖入口集合
对动态入口 D,其 dynamically dependent entries 是所有动态 importers(以及 implicitlyLoadedAfter)的 dependent entries 并集。
- 位置:
getDynamicallyDependentEntriesByDynamicEntrysrc/utils/chunkAssignment.ts:410-433
记 Dep(D) 为这些入口集合。
2) 关键命题(必然已加载原子)
对任意动态入口 D,在它被加载时,必然已加载的 atom 集合等于:
⋂_{e ∈ Dep(D)} ( staticDependencyAtomsByEntry[e] ∪ alreadyLoadedAtomsByEntry[e] )理由:
- 对任意 importer
e,其静态依赖staticDependencyAtomsByEntry[e]在e加载完成后必然在内存中。 - 若
e本身是动态入口,则其加载前必然已在内存中的 atom 为alreadyLoadedAtomsByEntry[e]。 - "必然已加载"必须对所有 importer 同时成立,因此取交集。
3) 迭代收敛(处理动态入口依赖动态入口)
源码通过迭代求固定点:
const alreadyLoadedAtomsByEntry: bigint[] = allEntries.map((_entry, entryIndex) =>
dynamicallyDependentEntriesByDynamicEntry.has(entryIndex) ? -1n : 0n
);
for (const [dynamicEntryIndex, dynamicallyDependentEntries] of dynamicallyDependentEntriesByDynamicEntry) {
dynamicallyDependentEntriesByDynamicEntry.delete(dynamicEntryIndex);
const knownLoadedAtoms = alreadyLoadedAtomsByEntry[dynamicEntryIndex];
let updatedLoadedAtoms = knownLoadedAtoms;
for (const entryIndex of dynamicallyDependentEntries) {
updatedLoadedAtoms &=
staticDependencyAtomsByEntry[entryIndex] | alreadyLoadedAtomsByEntry[entryIndex];
}
if (updatedLoadedAtoms !== knownLoadedAtoms) {
alreadyLoadedAtomsByEntry[dynamicEntryIndex] = updatedLoadedAtoms;
for (const dynamicImport of dynamicImportsByEntry[dynamicEntryIndex]) {
getOrCreate(
dynamicallyDependentEntriesByDynamicEntry,
dynamicImport,
getNewSet<number>
).add(dynamicEntryIndex);
}
}
}- 初始化为
-1n表示未知(所有位为 1),迭代过程中不断收紧。 - 若某动态入口结果变化,会重新标记其依赖动态入口进行更新,直至收敛。
4) 依赖标记移除的正确性
若某 atom A 满足:
A ∈ alreadyLoadedAtomsByEntry[D]则 A 在任何触发 D 的路径下都已在内存中。因此移除 A 上的 D 依赖标记不会导致缺失加载,反而避免生成冗余共享 chunk。
对应源码:
if (
(alreadyLoadedAtomsByEntry[entryIndex] & chunkMask) === chunkMask &&
(awaitedAlreadyLoadedAtomsByEntry[entryIndex] & chunkMask) === 0n
) {
dependentEntries.delete(entryIndex);
}复现实例(entry => b;entry -> s;b -> s)
1) dependent entries
entry→b→s→
2) 初始 atom
{E}:entry{B}:b{E,B}:s
3) 已加载原子
b的 importer 是entryentry静态可达原子包含entry与s- ⇒
alreadyLoadedAtoms[b]包含{E}与{E,B}
4) 移除标记与最终结果
s的 dependent entries 从{E,B}→{E}- 重新聚类后:
{E}→entry + s{B}→b
结论:s 不再进入动态入口 chunk,而被并入静态入口侧。
影响 chunk 划分的配置项(范围 / 时机 / 作用)
仅列出会改变 chunk 划分结果的配置项。
Documentation Note
onlyExplicitManualChunks 在 Rollup 4 中引入,并在官方文档中标注为弃用项(预期 Rollup 5 会将函数形式的默认行为改为"仅包含显式模块")。该说明来自官方文档而非源码注释。
重要边界条件
awaited 动态导入:
top-level await相关动态导入不会移除依赖标记,以避免输出 chunk 产生循环依赖(removeUnnecessaryDependentEntries的第二条件)。manualChunks 与
experimentalMinChunkSize:experimentalMinChunkSize仅对自动 chunk 生效,不会重写 manual chunk。preserveModules与inlineDynamicImports: 两者会直接绕过默认分块算法,动态入口优化与 minChunkSize 合并均不适用。
可复现验证路径(建议)
- 在 Rollup 源码中定位
Bundle.generateChunks,确认分支路径与参数传递。 - 在
chunkAssignment.ts中逐步跟踪:analyzeModuleGraphgetChunksWithSameDependentEntriesgetAlreadyLoadedAtomsByEntry/removeUnnecessaryDependentEntriesgetOptimizedChunks
- 以本文示例依赖图验证 dependent entries 与最终 chunk 划分结果。
结论(客观表述)
Rollup 的默认多 chunk 划分算法先按 dependent entries 生成原子 chunk,再通过动态入口"必然已加载原子"优化移除依赖标记,从而避免生成冗余共享 chunk;随后在满足副作用与依赖约束的前提下执行 experimentalMinChunkSize 合并优化。不同配置项会在不同阶段改变参与分块的模块集合或分块规则,从而影响最终 chunk 结构。