#10886·rolldown

entriesAwareMergeThreshold makes code-splitting non-deterministic: same input, different chunk names every build

Author: ThibautCuchetCreated Sep 15, 2026Updated Sep 16, 2026
Labelsneeds-reproduction

Version

rolldown 1.0.3 (as pinned by Vite 8.0.16), also reproduced on 1.2.0 and 1.2.5.

Summary

entriesAwareMergeThreshold makes output.codeSplitting non-deterministic. Two builds of the same input, in the same process-free sequence, put the same modules in different subgroups and in a different order within a subgroup. Chunk bytes change, so the content hash changes, so the file name changes, and every importer's import specifier follows.

entriesAware on its own is deterministic. Only the sub-threshold merge is affected: with entriesAwareMergeThreshold: 0 the output is byte-stable across runs.

Reproduction

400 icon-like modules (~1 KB each); 40 entries each statically importing 25 of them at random (fixed seed); a 41st entry holding one () => import() per icon, the way lucide-react's dynamic icon map does.

javascript
// build.mjs
import { rolldown } from "rolldown";
import { readdirSync, rmSync } from "node:fs";

const out = process.argv[2];
rmSync(out, { recursive: true, force: true });
const bundle = await rolldown({
  input: Object.fromEntries(readdirSync("src").map((f) => [f.replace(/\.js$/, ""), `./src/${f}`])),
});
await bundle.write({
  dir: out,
  chunkFileNames: "chunk-[hash].js",
  codeSplitting: {
    groups: [{ name: "icons", test: /[\\/]icons[\\/]/, entriesAware: true, entriesAwareMergeThreshold: 10000 }],
  },
});
console.log(readdirSync(out).sort().join("\n"));
bash
node build.mjs out1 > r1.txt
node build.mjs out2 > r2.txt
comm -12 <(sort r1.txt) <(sort r2.txt) | grep -c '^chunk-'

Expected

Every chunk name is identical between the two runs.

Actual

The threshold governs how much churn there is. Chunk names shared between two runs of the same input:

entriesAwareMergeThreshold chunks identical names across two runs
0 401 401 / 401
1 401 401 / 401
1100 330 261 / 330
2500 103 3 / 103
5000 53 1 / 53
10000 28 1 / 28

Identical on 1.2.0 (1 / 28) and 1.2.5 (1 / 26).

What the divergence looks like

Two chunks from two runs hold the same module set and declare the same export surface, but order the modules differently, so the mangled local names shift:

javascript
// run 1
export{n as _,F as a,E as c,_ as d,m as f,a as g,c as h,R as i,C as l,u as m,W as n,M as o,d as p,V as r,k as s,q as t,b as u}
// run 2
export{n as _,F as a,E as c,_ as d,m as f,i as g,a as h,R as i,C as l,c as m,W as n,M as o,d as p,V as r,k as s,q as t,b as u}

An importer then changes too, because both the host chunk name and the imported binding letter moved:

javascript
// run 1
import{m as r}from"./chunk-BN1Ekb1c.js";
// run 2
import{g as r}from"./chunk-DE293plX.js";

Impact

In a real app (Vite 8.0.16, 2655 client chunks, lucide-react in an entriesAware group), two builds of the same commit share 812 of 2655 chunk names. Removing entriesAware gives 2572 / 2572; setting entriesAwareMergeThreshold: 0 gives 4238 / 4238. With the merge on, no CDN or browser can hold a warm copy of a chunk across deployments.