Skip to content

Rollup Chunk Assignment: Default Algorithm and Config Impact (Source-Level Analysis)

Overview

This article focuses on Rollup's chunk assignment algorithm, dynamic entry optimization, and configuration impact scope in the default multi-chunk path, with reproducible conclusions and verification paths grounded in source code.

This article does not cover chunk naming or output path rules (e.g. chunkFileNames). It only covers factors that change chunk assignment results.


Reproducible baseline (version and scope)

  • Source baseline (current Rollup repo commit): c79e6c201d1f99e126d2e6bfb3f8c5c100ddcebf
  • Key files:
    • src/Bundle.ts
    • src/utils/chunkAssignment.ts

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


Scope and default assumptions

This article discusses the default multi-chunk path:

  • output.inlineDynamicImports = false
  • output.preserveModules = false
  • output.manualChunks not used (or not affecting this case)

If you enable inlineDynamicImports / preserveModules / manualChunks, the flow and results change (see "Config impact" below).


Core concepts (aligned with source)

  • entry: the static entry module from Rollup input.
  • dynamic entry: a dynamically imported target module; added to allEntries during chunking.
  • dependent entries: which entries can reach a module statically.
  • atom: a set of modules with the same dependent entries.
  • staticDependencyAtomsByEntry: atoms statically reachable from an entry (BigInt bitset).
  • alreadyLoadedAtomsByEntry: atoms that are "guaranteed in memory" when an entry is loaded (BigInt bitset).

Default chunk assignment algorithm (execution order)

Default Chunking Flow

EXEC
Entry Path Selection
Initialize
Selects between inlineDynamicImports / preserveModules / getChunkAssignments at the Bundle layer
Bundle.ts:158-192
manualChunks Pre-assignment
Initialize
Generates manual chunk definitions and excludes corresponding modules from default chunking
chunkAssignment.ts:222-267
Module Graph Analysis
Analyze
Computes dependent entries and collects dynamic entries
chunkAssignment.ts:269-357
Atom Grouping
Analyze
Groups modules with identical dependent entries into atoms
chunkAssignment.ts:436-450
Dynamic Entry Optimization
Optimize
Computes alreadyLoadedAtoms and removes redundant dependent entry markers
chunkAssignment.ts:491-559
Reclustering
Optimize
Generates initial chunks based on updated dependent entries
chunkAssignment.ts:562-627
Minimum Size Merge
Optimize
Merges small chunks under side effect and dependency constraints (optional)
chunkAssignment.ts:740-769
Output Ready
Initialize
Analyze
Optimize

1) Entry path selection (Bundle-level branch)

Bundle.generateChunks calls getChunkAssignments in the default path.

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
      );
  • Location: src/Bundle.ts:158-192

2) manualChunks pre-allocation

If manualChunks is configured, it first builds chunkDefinitions and marks modulesInManualChunks. The default algorithm then skips those modules.

ts
if (isManualChunksFunctionForm && onlyExplicitManualChunks) {
  (manualChunkModulesByAlias[alias] ||= []).push(entry);
} else {
  addStaticDependenciesToManualChunk(
    entry,
    (manualChunkModulesByAlias[alias] ||= []),
    modulesInManualChunks
  );
}
  • Location: src/utils/chunkAssignment.ts:222-267

3) Graph construction and dependent entries

Walk entry modules and static deps to record dependent entries for each module. Dynamic import targets are added to 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);
  }
}
  • Location: src/utils/chunkAssignment.ts:269-357

4) Atom grouping

Modules with the same dependent entries are merged into atoms.

ts
let chunkSignature = 0n;
for (const entryIndex of dependentEntries) {
  chunkSignature |= 1n << BigInt(entryIndex);
}
(chunkModules[String(chunkSignature)] ||= {
  dependentEntries: new Set(dependentEntries),
  modules: []
}).modules.push(...modules);
  • Location: src/utils/chunkAssignment.ts:436-450

5) Dynamic entry "already-loaded atoms" optimization

Compute atoms that are guaranteed to be in memory when a dynamic entry loads, and remove the corresponding dependent entry markers (see "Proof flow" below).

  • Intersection and iteration logic: src/utils/chunkAssignment.ts:491-533
  • Marker removal logic: src/utils/chunkAssignment.ts:536-559

6) Re-cluster to produce initial chunks

After updating dependent entries, re-cluster to produce initial chunks.

  • Location: src/utils/chunkAssignment.ts:562-627

7) experimentalMinChunkSize merge optimization

If a minimum chunk size threshold is enabled, Rollup attempts to merge small chunks while respecting side-effect and dependency constraints.

  • Location: src/utils/chunkAssignment.ts:740-769

For the full merge algorithm analysis (greedy loop, side-effect constraints, cycle checks, etc.), see experimentalMinChunkSize.


Proof flow for dynamic entry optimization (core logic)

Goal: prove that "compute already-loaded atoms for a dynamic entry D and remove dependent entry markers" is correct and safe.

1) Dynamically dependent entry set for a dynamic entry

For a dynamic entry D, its dynamically dependent entries are the union of dependent entries for all dynamic importers (and implicitlyLoadedAfter).

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

Let Dep(D) denote this entry set.

2) Key proposition (already-loaded atoms)

For any dynamic entry D, the set of atoms guaranteed to be loaded when D is loaded equals:

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

Rationale:

  1. For any importer e, staticDependencyAtomsByEntry[e] is guaranteed to be in memory after e finishes loading.
  2. If e itself is a dynamic entry, the atoms guaranteed to be in memory before it loads are alreadyLoadedAtomsByEntry[e].
  3. "Guaranteed to be loaded" must hold for all importers, so take the intersection.

3) Iterative convergence (dynamic entries depending on dynamic entries)

The source computes a fixed point via iteration:

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 means unknown (all bits are 1). Each iteration tightens the set.
  • If a dynamic entry's result changes, its dependent dynamic entries are re-marked for update until convergence.

4) Correctness of removing dependent entry markers

If an atom A satisfies:

text
A ∈ alreadyLoadedAtomsByEntry[D]

then A is in memory for any path that triggers D. Removing the D marker on A will not cause missing loads and instead avoids redundant shared chunks.

Corresponding source:

ts
if (
  (alreadyLoadedAtomsByEntry[entryIndex] & chunkMask) === chunkMask &&
  (awaitedAlreadyLoadedAtomsByEntry[entryIndex] & chunkMask) === 0n
) {
  dependentEntries.delete(entryIndex);
}

Reproducible example (entry => b; entry -> s; b -> s)

1) dependent entries

  • entry ->
  • b ->
  • s ->

2) Initial atoms

  • {E}: entry
  • {B}: b
  • {E,B}: s

3) Already-loaded atoms

  • The importer of b is entry
  • entry's statically reachable atoms include entry and s
  • => alreadyLoadedAtoms[b] includes {E} and {E,B}

4) Remove markers and final result

  • s's dependent entries change from {E,B} -> {E}
  • After re-clustering:
    • {E} -> entry + s
    • {B} -> b

Conclusion: s no longer goes into the dynamic entry chunk and is merged into the static entry side.


Config options that affect chunk assignment (scope / timing / effect)

Only lists config options that change chunk assignment results.

SPEC
Configuration
Scope
Timing
Mechanism
input
Entire graph
Entry list construction
Entry set determines dependent entries calculation and subsequent chunking baseline
output.inlineDynamicImports
Entire graph
Before default chunking
Forces single chunk, bypasses default chunking algorithm
output.preserveModules
Entire graph
Before default chunking
One chunk per module, bypasses default chunking algorithm
output.manualChunks
Matched modules and deps
Pre-assignment phase
Pre-assigns manual chunk definitions; related modules excluded from default chunking
output.onlyExplicitManualChunks
Function form only
Pre-assignment phase
When true, includes only explicitly returned modules; when false, includes static dependencies
output.experimentalMinChunkSize
Auto chunks only
After default chunking
Merges small chunks under side effect and dependency constraints
treeshake / moduleSideEffects
Auto chunks
Inclusion and merge checks
Affects whether modules participate in chunking and side effect evaluation

Documentation Note

onlyExplicitManualChunks was introduced in Rollup 4 and is marked as deprecated in the official docs (Rollup 5 is expected to change the default behavior of the function form to "explicit modules only"). This note comes from the official docs, not from source comments.


Important boundary conditions

  1. awaited dynamic imports: Dynamic imports related to top-level await will not have their markers removed, to avoid creating circular dependencies in output chunks (the second condition in removeUnnecessaryDependentEntries).

  2. manualChunks and experimentalMinChunkSize: experimentalMinChunkSize only applies to automatic chunks and does not rewrite manual chunks.

  3. preserveModules and inlineDynamicImports: Both bypass the default chunk assignment algorithm, so dynamic entry optimization and minChunkSize merges do not apply.


  1. Locate Bundle.generateChunks in Rollup source to confirm the branch path and parameter passing.
  2. Step through in chunkAssignment.ts:
    • analyzeModuleGraph
    • getChunksWithSameDependentEntries
    • getAlreadyLoadedAtomsByEntry / removeUnnecessaryDependentEntries
    • getOptimizedChunks
  3. Validate dependent entries and final chunk assignment results using the dependency graph in this article.

Conclusion (objective statement)

Rollup's default multi-chunk assignment algorithm first creates atom chunks based on dependent entries, then removes dependent entry markers via the dynamic entry "already-loaded atoms" optimization to avoid redundant shared chunks. It then applies experimentalMinChunkSize merge optimization under side-effect and dependency constraints. Different config options change the module set or chunking rules at different phases, resulting in different final chunk structures.

Contributors

Changelog

Discuss

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