Skip to content

Rollup Chunk 划分:默认算法与配置影响(源码级分析)

Overview

本文聚焦 Rollup 在默认多 chunk 路径下的 chunk 划分算法动态入口优化配置项影响范围,并基于源码给出可复现的结论与验证路径。

本文不讨论 chunk 命名与输出路径规则(如 chunkFileNames),只讨论会改变 chunk 划分结果的因素。


可复现基线(版本与范围)

  • 源码基线(当前 Rollup 仓库提交):c79e6c201d1f99e126d2e6bfb3f8c5c100ddcebf
  • 关键文件:
    • src/Bundle.ts
    • src/utils/chunkAssignment.ts

结论严格基于以上版本源码,不保证适用于其他版本或分支。


适用范围与默认前提

本文讨论 默认多 chunk 路径

  • output.inlineDynamicImports = false
  • output.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 划分算法(按执行顺序)

默认分块流程

EXEC
入口路径选择
初始化
在 Bundle 层分支中选择 inlineDynamicImports / preserveModules / getChunkAssignments
Bundle.ts:158-192
manualChunks 预分配
初始化
生成 manual chunk 定义并将对应模块排除出默认分块
chunkAssignment.ts:222-267
模块图分析
分析
计算 dependent entries 并收集 dynamic entries
chunkAssignment.ts:269-357
原子分组
分析
将 dependent entries 相同的模块分组为 atom
chunkAssignment.ts:436-450
动态入口优化
优化
计算 alreadyLoadedAtoms 并移除冗余 dependent entry 标记
chunkAssignment.ts:491-559
重新聚类
优化
基于更新后的 dependent entries 生成初始 chunk
chunkAssignment.ts:562-627
最小尺寸合并
优化
在副作用与依赖约束下合并小 chunk(可选)
chunkAssignment.ts:740-769
Output Ready
初始化
分析
优化

1) 入口路径选择(Bundle 层分支)

Bundle.generateChunks 在默认路径下调用 getChunkAssignments

ts
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,后续默认算法会跳过这些模块。

ts
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

ts
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 的模块先合并为原子。

ts
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 并集。

  • 位置:getDynamicallyDependentEntriesByDynamicEntry src/utils/chunkAssignment.ts:410-433

Dep(D) 为这些入口集合。

2) 关键命题(必然已加载原子)

对任意动态入口 D,在它被加载时,必然已加载的 atom 集合等于:

text
⋂_{e ∈ Dep(D)} ( staticDependencyAtomsByEntry[e] ∪ alreadyLoadedAtomsByEntry[e] )

理由:

  1. 对任意 importer e,其静态依赖 staticDependencyAtomsByEntry[e]e 加载完成后必然在内存中。
  2. e 本身是动态入口,则其加载前必然已在内存中的 atom 为 alreadyLoadedAtomsByEntry[e]
  3. "必然已加载"必须对所有 importer 同时成立,因此取交集。

3) 迭代收敛(处理动态入口依赖动态入口)

源码通过迭代求固定点:

ts
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 满足:

text
A ∈ alreadyLoadedAtomsByEntry[D]

A 在任何触发 D 的路径下都已在内存中。因此移除 A 上的 D 依赖标记不会导致缺失加载,反而避免生成冗余共享 chunk。

对应源码:

ts
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 是 entry
  • entry 静态可达原子包含 entrys
  • alreadyLoadedAtoms[b] 包含 {E}{E,B}

4) 移除标记与最终结果

  • s 的 dependent entries 从 {E,B}{E}
  • 重新聚类后:
    • {E}entry + s
    • {B}b

结论:s 不再进入动态入口 chunk,而被并入静态入口侧。


影响 chunk 划分的配置项(范围 / 时机 / 作用)

仅列出会改变 chunk 划分结果的配置项。

SPEC
配置项
影响范围
影响时机
影响机制
input
全图
构建入口列表阶段
入口集合决定 dependent entries 计算与后续分块基准
output.inlineDynamicImports
全图
默认分块之前
强制单一 chunk,绕过默认分块算法
output.preserveModules
全图
默认分块之前
每模块一个 chunk,绕过默认分块算法
output.manualChunks
命中的模块及其依赖
预分配阶段
预分配 manual chunk 定义,相关模块不再参与默认分块
output.onlyExplicitManualChunks
仅函数形式
预分配阶段
true 时仅包含显式返回模块;false 时包含静态依赖
output.experimentalMinChunkSize
仅自动 chunk
默认分块之后
在副作用与依赖约束下合并小 chunk
treeshake / moduleSideEffects
自动 chunk
包含判定与合并检查
影响模块是否参与分块与副作用评估

Documentation Note

onlyExplicitManualChunks 在 Rollup 4 中引入,并在官方文档中标注为弃用项(预期 Rollup 5 会将函数形式的默认行为改为"仅包含显式模块")。该说明来自官方文档而非源码注释。


重要边界条件

  1. awaited 动态导入top-level await 相关动态导入不会移除依赖标记,以避免输出 chunk 产生循环依赖(removeUnnecessaryDependentEntries 的第二条件)。

  2. manualChunks 与 experimentalMinChunkSizeexperimentalMinChunkSize 仅对自动 chunk 生效,不会重写 manual chunk。

  3. preserveModulesinlineDynamicImports: 两者会直接绕过默认分块算法,动态入口优化与 minChunkSize 合并均不适用。


可复现验证路径(建议)

  1. 在 Rollup 源码中定位 Bundle.generateChunks,确认分支路径与参数传递。
  2. chunkAssignment.ts 中逐步跟踪:
    • analyzeModuleGraph
    • getChunksWithSameDependentEntries
    • getAlreadyLoadedAtomsByEntry / removeUnnecessaryDependentEntries
    • getOptimizedChunks
  3. 以本文示例依赖图验证 dependent entries 与最终 chunk 划分结果。

结论(客观表述)

Rollup 的默认多 chunk 划分算法先按 dependent entries 生成原子 chunk,再通过动态入口"必然已加载原子"优化移除依赖标记,从而避免生成冗余共享 chunk;随后在满足副作用与依赖约束的前提下执行 experimentalMinChunkSize 合并优化。不同配置项会在不同阶段改变参与分块的模块集合或分块规则,从而影响最终 chunk 结构。

根据 CC BY-SA 4.0 许可证发布。 (134a8ec)