Inline Common Chunks
Inline Common Chunks reduces the number of small common chunks generated by Automatic Code Splitting. Rolldown copies their code into the chunks that use them and removes the separate common chunk files. A shared runtime makes the copies use the same module state and exports.
Usage
import { defineConfig } from 'rolldown';
export default defineConfig({
input: ['src/a.js', 'src/b.js', 'src/c.js'],
preserveEntrySignatures: false,
output: {
dir: 'dist',
format: 'es',
codeSplitting: {
experimentalInlineCommonChunks: {
maxSize: 10 * 1024, // < 10 KiB
},
},
},
});maxSize uses transformed source sizes, summed per chunk before rendering, minification and compression. It defaults to 0 (disabled); Infinity removes the size limit.
Entries, dynamic import targets and manual chunks are not inlined. Matching any module with exclude prevents its entire common chunk from being inlined.
Inlining requires:
- ESM output (
output.format: 'es'). preserveEntrySignatures: false, which allows unused entry exports to be removed.strictExecutionOrderenabled. Rolldown enables it automatically when omitted.
How it works
Automatic code splitting follows two rules inherited from Rollup's output model:
- A module's code appears in only one output chunk.
- An entry only loads application modules that are reachable from that entry.
The configuration in Usage defines three entries: A, B and C. All three import a large library; A and B also import a small helper with a counter:
import './library.js';
import { inc } from './helper.js';
console.log(inc());import './library.js';console.log('helper initialized');
let count = 0;
export function inc() {
return ++count;
}The library and helper go into different common chunks because they have different sets of importing entries, regardless of size. Dynamic import targets count as entries too, so the same situation arises between lazy routes.
Manual code splitting could merge them, but then C would load the helper. Inlining copies the helper into A and B. With the helper below maxSize and the library above it:
Arrows represent imports; the shared runtime is omitted.
Sharing state between copies
Loading A and then B in the same JavaScript environment produces:
helper initialized
1
2Pasting the helper's body into both files would create two counters, both starting at 0. Each file would log helper initialized and 1. Exported objects and functions would also have different identities.
Rolldown wraps the common chunk in a factory function and registers each copy with a shared runtime. Both A and B contain this code (names simplified; library import and entry wrappers omitted):
import {
__share,
__share_require,
__share_export,
__esmMin,
} from './rolldown-runtime.js';
__share('helper', (exports) => {
let count;
function inc() {
return ++count;
}
const init_helper = __esmMin(() => {
console.log('helper initialized');
count = 0;
});
__share_export(exports, {
inc: () => inc,
init_helper: () => init_helper,
});
});
const helper = __share_require('helper');
helper.init_helper();
console.log((0, helper.inc)());| Runtime helper | Behavior |
|---|---|
__share | Registers a factory without calling it. The first registration for an identifier wins. |
__share_require | Runs the factory on first use and caches its exports object. |
__esmMin | Makes the module initializer run once. |
__share_export | Exposes exports through getters, preserving live bindings. |
A's call to init_helper initializes the shared count, and inc() returns 1. B gets the same functions: its initializer does nothing, and inc() returns 2.
Separating registration from module initialization makes factories available before consumers run, while strictExecutionOrder controls when the module's top-level code executes.
All copies must import the same runtime chunk to share state.
Loading and caching
Registering the helper from A does not remove the copy embedded in B's file; fetching B still transfers that code.
| Loading scenario | Separate helper chunk | Helper inlined into A and B |
|---|---|---|
| Cold load of A | Fetch the helper separately. | Fetch the helper inside A. |
| Then load B | Reuse the loaded helper. | Fetch another copy inside B. |
| Load an updated A; the unchanged helper is cached | Reuse the cached helper. | Fetch the helper again inside the new A. |
Saving a request may justify duplicating a small helper. With maxSize: Infinity, the same tradeoff applies to the large library shared by A, B and C: if it is eligible for inlining, loading all three entries transfers three copies of the library. The runtime also adds a shared file and execution overhead.
maxSize tests a chunk's source size, not the total bytes its copies add to a user's session. Fewer chunks therefore do not guarantee faster loading. Compare loading times with and without inlining for initial loads and later navigation in your application, including cache reuse, before choosing whether to enable it or raise the threshold.
Optimizations
Avoiding unnecessary copies
A and B each need a factory because either entry can load first. Now suppose both can dynamically import detail.js, which also uses the helper. By the time either import starts, A or B has already registered the factory, so detail.js does not need another copy:
import { __share_require } from './rolldown-runtime.js';
const helper = __share_require('helper');
helper.init_helper();
console.log((0, helper.inc)());Rolldown omits a copy only when every known path from a configured or plugin-emitted entry supplies the factory first. An earlier static dependency can supply it too.
Keeping modules in the same scope
Rolldown wraps the whole common chunk in one factory, so its modules can refer to each other directly. A minifier can rename and inline code within that scope. References from outside the factory go through its exports object, which limits optimization across the boundary.
Compatibility
Chunk selection
A common chunk is not inlined, even below maxSize, if:
- It contains top-level await or statically depends on a module that does.
- It contains
import.metathat remains after tree shaking. - It contains an unresolved dynamic import or imports an external module.
- Another output chunk re-exports one of its bindings.
- It contains declaration modules or builtin
assetorcopymodules.
Direct eval and static cycles involving a chunk that is not inlined can also prevent inlining.
Plugins
An inlined module appears in the
moduleIdsandmodulesof every chunk containing a copy. CSS extraction and SSR manifest plugins must account for a module belonging to multiple chunks.Vite
For Vite builds, add this
excludematcher toexperimentalInlineCommonChunksto prevent CSS from being duplicated across output stylesheets:jsexclude: /\.(css|scss|sass|less|styl|stylus|pcss|postcss|sss)(\?|$)/,If a
renderChunkhook gives copies different values, the first registered copy determines the value for all consumers. Useexcludefor affected modules.
For more background, see the design RFC. Application results and bug reports are welcome in the feedback issue.