Skip to content

output.experimentalMinChunkSize

概述

experimentalMinChunkSize 设置一个最小 chunk 大小阈值(单位:字节),Rollup 会尝试将低于该阈值的自动 chunk 合并到其他 chunk 中,以减少输出文件数量。

javascript
// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'es',
    experimentalMinChunkSize: 1000
  }
};

适用范围

  • 仅对自动 chunk 生效,不会重写 manualChunks 定义的 chunk。
  • 仅在默认多 chunk 路径下运行(inlineDynamicImports = falsepreserveModules = false)。
  • 在 chunk 划分算法的最后阶段执行,位于动态入口优化与重新聚类之后。

关于 experimentalMinChunkSize 在整体 chunk 划分流程中的位置,参见 Chunk 划分算法


可复现基线

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

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


合并算法详解

MINCHUNK 合并流程

MERGE
分区划分
处理
将 chunk 分为 big(≥ minChunkSize)和 small(< minChunkSize)两类
chunkAssignment.ts:771-789
排序
处理
按 size 升序排列小 chunk
chunkAssignment.ts:783
遍历小 chunk
处理
依次取出最小的小 chunk 尝试合并
chunkAssignment.ts:805
寻找最佳目标
处理
遍历所有候选 chunk,计算合并成本
chunkAssignment.ts:844-869
副作用检查
决策
验证合并不会引入非关联副作用
PASS: 继续FAIL: Infinity
chunkAssignment.ts:914-918
循环检测
决策
确保合并不会产生 chunk 间循环依赖
PASS: 继续FAIL: Infinity
chunkAssignment.ts:927-929
计算额外大小
处理
统计合并后引入的未使用代码大小
chunkAssignment.ts:934-938
执行合并
处理
若找到有效目标,执行合并并更新依赖图
chunkAssignment.ts:814-839
输出结果
终止
返回合并后的 chunk 列表
chunkAssignment.ts:768
Chunks Optimized
处理
决策
终止
处理节点
决策节点
终止节点

1) 算法概述

合并优化采用贪心算法,目标是减少小 chunk 数量,同时保证:

  1. 副作用不变式:合并后各入口的 correlated side effects 保持不变
  2. 无循环依赖:合并不会产生 chunk 间的循环依赖
  3. 最小化额外加载:优先选择引入最少额外代码的合并目标

入口函数 getOptimizedChunks 协调整个合并流程:

ts
function getOptimizedChunks(
  chunks: ChunkDescription[],
  minChunkSize: number,
  sideEffectAtoms: bigint,
  sizeByAtom: number[],
  log: LogHandler
): { modules: Module[] }[] {
  timeStart('optimize chunks', 3);
  const chunkPartition = getPartitionedChunks(chunks, minChunkSize);
  if (!chunkPartition) {
    timeEnd('optimize chunks', 3);
    return chunks;
  }
  // ... logging
  mergeChunks(chunkPartition, minChunkSize, sideEffectAtoms, sizeByAtom);
  // ...
  return [...chunkPartition.small, ...chunkPartition.big];
}
  • 位置:src/utils/chunkAssignment.ts:740-769

2) 核心数据结构

ChunkDescription 描述一个待合并的 chunk:

ts
interface ChunkDescription extends ModulesWithDependentEntries {
  containedAtoms: bigint;      // 本 chunk 包含的原子(位掩码)
  correlatedAtoms: bigint;     // 加载本 chunk 时必然已加载的原子
  dependencies: Set<ChunkDescription>;
  dependentChunks: Set<ChunkDescription>;
  pure: boolean;               // 是否无副作用
  size: number;
}

ChunkPartition 将 chunk 分为大小两类:

ts
interface ChunkPartition {
  big: Set<ChunkDescription>;   // size >= minChunkSize
  small: Set<ChunkDescription>; // size < minChunkSize
}
  • 位置:src/utils/chunkAssignment.ts:19-39

BigInt 位掩码:每个原子对应一个 bit 位置,例如 atom 0 = 1n,atom 1 = 2n,atom 2 = 4n。使用位运算进行高效集合操作。

3) 分区与排序

getPartitionedChunks 将 chunk 按 minChunkSize 阈值分为两类,并按 size 升序排列:

ts
function getPartitionedChunks(
  chunks: ChunkDescription[],
  minChunkSize: number
): ChunkPartition | null {
  const smallChunks: ChunkDescription[] = [];
  const bigChunks: ChunkDescription[] = [];
  for (const chunk of chunks) {
    (chunk.size < minChunkSize ? smallChunks : bigChunks).push(chunk);
  }
  if (smallChunks.length === 0) {
    return null;  // 无小 chunk,跳过合并
  }
  smallChunks.sort(compareChunkSize);
  bigChunks.sort(compareChunkSize);
  return {
    big: new Set(bigChunks),
    small: new Set(smallChunks)
  };
}
  • 位置:src/utils/chunkAssignment.ts:771-789

4) 贪心合并循环

mergeChunks 按 size 从小到大遍历小 chunk,为每个找最佳合并目标:

ts
function mergeChunks(
  chunkPartition: ChunkPartition,
  minChunkSize: number,
  sideEffectAtoms: bigint,
  sizeByAtom: number[]
) {
  const { small } = chunkPartition;
  for (const mergedChunk of small) {
    const bestTargetChunk = findBestMergeTarget(
      mergedChunk,
      chunkPartition,
      sideEffectAtoms,
      sizeByAtom,
      minChunkSize <= 1 ? 1 : Infinity
    );
    if (bestTargetChunk) {
      // 执行合并...
    }
  }
}

合并时的属性更新规则:

属性更新方式原因
modules拼接合并所有模块
size求和累加代码大小
pure逻辑与只有双方都无副作用才保持 pure
correlatedAtoms取交集合并后必然已加载的原子更少
containedAtoms取并集包含双方所有原子
dependentEntries取并集合并依赖入口

关键:correlatedAtoms 取交集确保合并后副作用不变式仍然成立。

  • 位置:src/utils/chunkAssignment.ts:798-842

5) 副作用约束详解

合并的核心约束是不引入非关联副作用。在 getAdditionalSizeIfNoTransitiveDependencyOrNonCorrelatedSideEffect 中检查:

ts
const { correlatedAtoms } = dependencyChunk;
let dependencyAtoms = dependentChunk.containedAtoms;
const dependentContainedSideEffects = dependencyAtoms & sideEffectAtoms;
if ((correlatedAtoms & dependentContainedSideEffects) !== dependentContainedSideEffects) {
  return Infinity;  // 合并会引入非关联副作用,拒绝
}

数学表达式:

合并有效条件:
  dependentChunk.containedSideEffects ⊆ dependencyChunk.correlatedAtoms

即位运算表示:
  (containedAtoms & sideEffectAtoms) & ~correlatedAtoms === 0n

解释:若 dependentChunk 包含的副作用不是 dependencyChunk 的 correlated side effects 的子集,则合并会导致某些入口加载时意外执行额外副作用,违反副作用不变式。

  • 位置:src/utils/chunkAssignment.ts:914-918

6) 循环检测机制

合并可能引入 chunk 间循环依赖,必须检测并拒绝。使用 BFS 遍历依赖图:

ts
const chunksToCheck = new Set(dependentChunk.dependencies);
for (const { dependencies, containedAtoms } of chunksToCheck) {
  dependencyAtoms |= containedAtoms;
  const containedSideEffects = containedAtoms & sideEffectAtoms;
  if ((correlatedAtoms & containedSideEffects) !== containedSideEffects) {
    return Infinity;  // 传递依赖包含非关联副作用
  }
  for (const dependency of dependencies) {
    if (dependency === dependencyChunk) {
      return Infinity;  // 发现循环!
    }
    chunksToCheck.add(dependency);
  }
}

原理:若 dependentChunk 的传递依赖中包含 dependencyChunk,则合并后会形成循环:mergedChunk → dependencyChunk → ... → mergedChunk

  • 位置:src/utils/chunkAssignment.ts:920-932

7) 大小计算与目标选择

getAdditionalSizeAfterMerge 双向检查合并成本:

ts
function getAdditionalSizeAfterMerge(
  mergedChunk: ChunkDescription,
  targetChunk: ChunkDescription,
  currentAdditionalSize: number,
  sideEffectAtoms: bigint,
  sizeByAtom: number[]
): number {
  const firstSize = getAdditionalSizeIfNoTransitiveDependencyOrNonCorrelatedSideEffect(
    mergedChunk, targetChunk, currentAdditionalSize, sideEffectAtoms, sizeByAtom
  );
  return firstSize < currentAdditionalSize
    ? firstSize + getAdditionalSizeIfNoTransitiveDependencyOrNonCorrelatedSideEffect(
        targetChunk, mergedChunk, currentAdditionalSize - firstSize, sideEffectAtoms, sizeByAtom
      )
    : Infinity;
}

额外大小计算:

ts
return getAtomsSizeIfBelowLimit(
  dependencyAtoms & ~correlatedAtoms,  // 未被关联的原子
  currentAdditionalSize,
  sizeByAtom
);

findBestMergeTarget 遍历所有候选,选择额外大小最小的目标:

ts
for (const targetChunk of concatLazy([small, big])) {
  if (mergedChunk === targetChunk) continue;
  const additionalSizeAfterMerge = getAdditionalSizeAfterMerge(...);
  if (additionalSizeAfterMerge < smallestAdditionalSize) {
    bestTargetChunk = targetChunk;
    if (additionalSizeAfterMerge === 0) break;  // 提前退出优化
    smallestAdditionalSize = additionalSizeAfterMerge;
  }
}
  • 位置:src/utils/chunkAssignment.ts:844-869, 879-939

重要边界条件

  1. manualChunks 不受影响experimentalMinChunkSize 仅对自动 chunk 生效,不会重写 manualChunks 定义的 chunk。

  2. preserveModulesinlineDynamicImports 下不生效:两者会直接绕过默认分块算法,experimentalMinChunkSize 合并不适用。

  3. 副作用严格约束:即使 chunk 低于阈值,若合并会引入非关联副作用或循环依赖,该 chunk 仍保持独立。

  4. 默认行为minChunkSize1):默认值为 1,此时仅合并额外大小为 0 的 chunk(即合并不增加任何冗余代码)。

贡献者

页面历史

Discuss

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