Skip to content

output.experimentalMinChunkSize

Overview

experimentalMinChunkSize sets a minimum chunk size threshold (in bytes). Rollup will attempt to merge automatic chunks below this threshold into other chunks to reduce the number of output files.

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

Scope

  • Only applies to automatic chunks; does not rewrite chunks defined by manualChunks.
  • Only runs under the default multi-chunk path (inlineDynamicImports = false and preserveModules = false).
  • Executes in the final phase of the chunk assignment algorithm, after dynamic entry optimization and re-clustering.

For the position of experimentalMinChunkSize in the overall chunk assignment flow, see Chunk Assignment Algorithm.


Reproducible Baseline

  • Source code baseline (current Rollup repository commit): c79e6c201d1f99e126d2e6bfb3f8c5c100ddcebf
  • Key file: src/utils/chunkAssignment.ts

Conclusions are strictly based on the above version; not guaranteed to apply to other versions or branches.


Merge Algorithm Details

Min Chunk Merge Flow

MERGE
Partition
Process
Divides chunks into two categories: big (>= minChunkSize) and small (< minChunkSize)
chunkAssignment.ts:771-789
Sort
Process
Sorts small chunks by size in ascending order
chunkAssignment.ts:783
Iterate Small Chunks
Process
Sequentially processes the smallest chunks and attempts to merge them
chunkAssignment.ts:805
Find Best Target
Process
Iterates through all candidate chunks and calculates merge cost
chunkAssignment.ts:844-869
Side Effect Check
Decision
Verifies that merge will not introduce unrelated side effects
PASS: ContinueFAIL: Infinity
chunkAssignment.ts:914-918
Cycle Detection
Decision
Ensures that merge will not create circular dependencies between chunks
PASS: ContinueFAIL: Infinity
chunkAssignment.ts:927-929
Calculate Additional Size
Process
Calculates the size of unused code introduced after merge
chunkAssignment.ts:934-938
Execute Merge
Process
If a valid target is found, executes merge and updates the dependency graph
chunkAssignment.ts:814-839
Output Result
Terminal
Returns the list of merged chunks
chunkAssignment.ts:768
Chunks Optimized
Process
Decision
Terminal
Process Node
Decision Node
Terminal Node

1) Algorithm Overview

The merge optimization uses a greedy algorithm aimed at reducing the number of small chunks while ensuring:

  1. Side effect invariant: Correlated side effects for each entry remain unchanged after merging
  2. No circular dependencies: Merging does not create circular dependencies between chunks
  3. Minimize additional loading: Prefer merge targets that introduce the least additional code

The entry function getOptimizedChunks coordinates the entire merge flow:

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];
}
  • Location: src/utils/chunkAssignment.ts:740-769

2) Core Data Structures

ChunkDescription describes a chunk to be merged:

ts
interface ChunkDescription extends ModulesWithDependentEntries {
  containedAtoms: bigint;      // Atoms contained in this chunk (bitmask)
  correlatedAtoms: bigint;     // Atoms guaranteed to be loaded when this chunk loads
  dependencies: Set<ChunkDescription>;
  dependentChunks: Set<ChunkDescription>;
  pure: boolean;               // Whether the chunk has no side effects
  size: number;
}

ChunkPartition divides chunks into two categories by size:

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

BigInt bitmask: Each atom corresponds to a bit position, e.g., atom 0 = 1n, atom 1 = 2n, atom 2 = 4n. Bitwise operations enable efficient set operations.

3) Partitioning and Sorting

getPartitionedChunks divides chunks into two categories by the minChunkSize threshold and sorts them by size in ascending order:

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;  // No small chunks, skip merging
  }
  smallChunks.sort(compareChunkSize);
  bigChunks.sort(compareChunkSize);
  return {
    big: new Set(bigChunks),
    small: new Set(smallChunks)
  };
}
  • Location: src/utils/chunkAssignment.ts:771-789

4) Greedy Merge Loop

mergeChunks iterates through small chunks in ascending size order, finding the best merge target for each:

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) {
      // Execute merge...
    }
  }
}

Property update rules during merging:

PropertyUpdate MethodReason
modulesConcatenateMerge all modules
sizeSumAccumulate code size
pureLogical ANDOnly remains pure if both are side-effect-free
correlatedAtomsIntersectionFewer atoms are guaranteed to be loaded after merging
containedAtomsUnionContains all atoms from both chunks
dependentEntriesUnionMerge dependent entries

Key point: correlatedAtoms uses intersection to ensure the side effect invariant still holds after merging.

  • Location: src/utils/chunkAssignment.ts:798-842

5) Side Effect Constraint Details

The core constraint for merging is not introducing non-correlated side effects. This is checked in getAdditionalSizeIfNoTransitiveDependencyOrNonCorrelatedSideEffect:

ts
const { correlatedAtoms } = dependencyChunk;
let dependencyAtoms = dependentChunk.containedAtoms;
const dependentContainedSideEffects = dependencyAtoms & sideEffectAtoms;
if ((correlatedAtoms & dependentContainedSideEffects) !== dependentContainedSideEffects) {
  return Infinity;  // Merge would introduce non-correlated side effects, reject
}

Mathematical expression:

Merge validity condition:
  dependentChunk.containedSideEffects ⊆ dependencyChunk.correlatedAtoms

Bitwise representation:
  (containedAtoms & sideEffectAtoms) & ~correlatedAtoms === 0n

Explanation: If the side effects contained in dependentChunk are not a subset of dependencyChunk's correlated side effects, merging would cause some entries to unexpectedly execute additional side effects when loaded, violating the side effect invariant.

  • Location: src/utils/chunkAssignment.ts:914-918

6) Cycle Detection Mechanism

Merging may introduce circular dependencies between chunks, which must be detected and rejected. BFS traversal is used on the dependency graph:

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;  // Transitive dependency contains non-correlated side effects
  }
  for (const dependency of dependencies) {
    if (dependency === dependencyChunk) {
      return Infinity;  // Cycle detected!
    }
    chunksToCheck.add(dependency);
  }
}

Principle: If dependencyChunk is found in the transitive dependencies of dependentChunk, merging would create a cycle: mergedChunk → dependencyChunk → ... → mergedChunk.

  • Location: src/utils/chunkAssignment.ts:920-932

7) Size Calculation and Target Selection

getAdditionalSizeAfterMerge performs bidirectional merge cost checking:

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;
}

Additional size calculation:

ts
return getAtomsSizeIfBelowLimit(
  dependencyAtoms & ~correlatedAtoms,  // Non-correlated atoms
  currentAdditionalSize,
  sizeByAtom
);

findBestMergeTarget iterates through all candidates and selects the target with the smallest additional size:

ts
for (const targetChunk of concatLazy([small, big])) {
  if (mergedChunk === targetChunk) continue;
  const additionalSizeAfterMerge = getAdditionalSizeAfterMerge(...);
  if (additionalSizeAfterMerge < smallestAdditionalSize) {
    bestTargetChunk = targetChunk;
    if (additionalSizeAfterMerge === 0) break;  // Early exit optimization
    smallestAdditionalSize = additionalSizeAfterMerge;
  }
}
  • Location: src/utils/chunkAssignment.ts:844-869, 879-939

Important Edge Cases

  1. manualChunks unaffected: experimentalMinChunkSize only applies to automatic chunks; it does not rewrite chunks defined by manualChunks.

  2. Not effective under preserveModules and inlineDynamicImports: Both options bypass the default chunking algorithm, making experimentalMinChunkSize merging inapplicable.

  3. Strict side effect constraints: Even if a chunk is below the threshold, it remains independent if merging would introduce non-correlated side effects or circular dependencies.

  4. Default behavior (minChunkSize is 1): The default value is 1, which only merges chunks with an additional size of 0 (i.e., merging adds no redundant code).

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (134a8ec)