entriesAwareMergeThreshold makes code-splitting non-deterministic: same input, different chunk names every build
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.
// 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"));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:
// 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:
// 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.
Source: rolldown/rolldown