Advanced Chunks
advancedChunks
is a powerful feature that allows you to do manual chunking
to complement the automatic chunking
done by code splitting. This is useful when you want to optimize the loading performance of your application by splitting it into smaller, more manageable pieces.
Before reading this guide, you should first understand the code splitting feature of Rolldown. This guide will explain how advancedChunks
works and how to use it effectively.
Before we dive into the details, let's clarify some things first.
automatic chunking
andmanual chunking
are not contradictory. Usingmanual chunking
does not mean disablingautomatic chunking
. A module will be either captured byautomatic chunking
ormanual chunking
depending on your configuration, but not both. If a module is not captured bymanual chunking
, it will still be put into a chunk which is created byautomatic chunking
while respecting the rules we explained in the code splitting guide.
Why use advancedChunks
?
The automatic chunking
doesn't take loading performance or cache invalidation into account. It simply groups modules based on their static imports. This can lead to suboptimal chunking, where large chunks are created that may not be performant for loading or cause cache invalidation for every deployment.
How to use advancedChunks
?
Let's take a look at the following example:
// index.jsx
import * as ReactDom from 'react-dom';
import App from './App.jsx';
ReactDom.createRoot(document.getElementById('root')).render(
<App />,
);
// App.jsx
import * as React from 'react';
import { Button } from 'ui-lib';
export default function App() {
return <Button onClick={() => alert('Button clicked!')} />;
}
and you get the following output:
// node_modules/react/index.js
'React library code';
// node_modules/ui-lib/index.js
'UI library code';
// node_modules/react-dom/index.js
'ReactDOM library code';
// App.js
function App() {
return <Button onClick={() => alert('Button clicked!')} />;
}
// index.js
ReactDom.createRoot(document.getElementById('root')).render(<App />);
In this example,
- We used 3 libraries:
react
,react-dom
, andui-lib
. output-hash0.js
is the output file generated by Rolldown.hash0
is the hash of the output file, it changes if the content of the file changes.
Reduce cache invalidation
Let's talk about cache invalidation first. Cache invalidation here means that when you deploy a new version of your application, the browser will need to download the new version of the file. If the file is large, this can lead to a poor user experience.
For example, if you change the app.jsx
file:
function App() {
return <Button onClick={() => alert('Button clicked!')} />;
return <Button onClick={() => alert('Button clicked!!!')} />;
}
then naturally, you get a output-hash1.js
file with the same content as output-hash0.js
, except for the change in the App
function.
Now, if you deploy this new version of your application, the browser will need to download the entire output-hash1.js
file, even though only a small part of it has changed. This is because the hash of the file has changed, and the browser will treat it as a new file.
To solve this problem, we can use advancedChunks
to split output libraries into separate chunks, because they don't change frequently compared to application code.
export default {
// ... other configurations
advancedChunks: {
groups: [
{
test: /node_modules/,
name: 'libs',
},
],
},
};
By using the above advancedChunks
option, the output will look like this:
import ... from './libs-hash0.js';
// App.js
function App() {
return <Button onClick={() => alert("Button clicked!")} />;
}
// index.js
ReactDom.createRoot(document.getElementById("root")).render(<App />);
// node_modules/react/index.js
"React library code";
// node_modules/ui-lib/index.js
"UI library code";
// node_modules/react-dom/index.js
"ReactDOM library code";
export { ... };
For example, after you change the app.jsx
file
function App() {
return <Button onClick={() => alert('Button clicked!')} />;
return <Button onClick={() => alert('Button clicked!!!')} />;
}
you will get output like this:
import ... from './libs-hash0.js';
// App.js
function App() {
return <Button onClick={() => alert("Button clicked!!!")} />;
}
// index.js
ReactDom.createRoot(document.getElementById("root")).render(<App />);
// node_modules/react/index.js
"React library code";
// node_modules/ui-lib/index.js
"UI library code";
// node_modules/react-dom/index.js
"ReactDOM library code";
export { ... };
- The
libs-hash0.js
file is not changed, so the browser can use the cached version of the file. - The
output-hash1.js
file is changed, so the browser will download the new version of the file.
Improve loading performance
advancedChunks
can also be used to improve the loading performance of your application by splitting it into a practical number of chunks and taking advantage of browser's parallel loading capabilities.
In the previous example, we put all the libraries into a single chunk, which is not optimal for loading performance. If the libraries are too large, the browser will spend a long time downloading the chunk, which can lead to a poor user experience.
To solve this problem, we can use advancedChunks
to split the libraries into separate chunks, so that the browser can download them in parallel.
export default {
// ... other configurations
advancedChunks: {
groups: [
{
test: /node_modules\/react/,
name: 'react',
},
{
test: /node_modules\/react-dom/,
name: 'react-dom',
},
{
test: /node_modules\/ui-lib/,
name: 'ui-lib',
},
],
},
};
By using the above advancedChunks
option, the output will look like this:
import ... from './react-hash0.js';
import ... from './react-dom-hash0.js';
import ... from './ui-lib-hash0.js';
// App.js
function App() {
return <Button onClick={() => alert("Button clicked!")} />;
}
// index.js
ReactDom.createRoot(document.getElementById("root")).render(<App />);
"React library code";
export { ... };
"ReactDOM library code";
export { ... };
"UI library code";
export { ... };
Now, the libraries are split into separate chunks, and the browser can download them in parallel. This can significantly improve the loading performance of your application, especially if the libraries are large.
Limitations
Why there's always a runtime.js
chunk?
tl;dr: If you used advancedChunks
option, rolldown will forcefully generate a runtime.js
chunk to ensure that the runtime code is always executed before any other chunks.
The runtime.js
chunk is a special chunk that only contains the runtime code necessary for loading and executing your application. It is generated forcefully by the bundler to ensure that the runtime code is always executed before any other chunks.
Since advanced chunks allows you to move modules between chunks, it's easily to create a circular import in the output code. This can lead to a situation where the runtime code is not executed before the other chunks, causing errors in your application.
A example output code with circular import:
// first.js
import { __esm, __export, init_second, value$1 as value } from './second.js';
var first_exports = {};
__export(first_exports, { value: () => value$1 });
var value$1;
var init_first = __esm({
'first.js'() {
init_second();
// ...
},
});
export { first_exports, init_first, value$1 as value };
// main.js
import { first_exports, init_first } from './first.js';
import { __esm, init_second, second_exports } from './second.js';
var init_main = __esm({
'main.js'() {
init_first();
init_second();
// ...
},
});
init_main();
// second.js
import { init_first, value } from './first.js';
var __esm = '...';
var __export = '...';
var second_exports = {};
__export(second_exports, { value: () => value$1 });
var value$1;
var init_second = __esm({
'second.js'() {
init_first();
// ...
},
});
export { __esm, __export, init_second, second_exports, value$1 };
When we run node ./main.js
, the traversal order of the modules would be main.js
-> first.js
-> second.js
. The module execution order would be second.js
-> first.js
-> main.js
.
second.js
tries to call __esm
function before it gets initialized. This will lead to a runtime error which is trying to call undefined
as a function.
With forcefully generated runtime.js
, the bundler ensures any chunk that depends on runtime code would first load runtime.js
before executing itself. This guarantees that the runtime code is always executed before any other chunks, preventing circular import issues.
Why the dependencies of the captured module get captured too?
When a module is captured by a group, Rolldown will try to capture its dependencies recursively. This is because Rolldown is only allowed to mangle the exports of non-entry chunks by default.
For example, if you have the following code:
// entry.js
import { value } from './a.js';
console.log(value);
export const foo = 'foo';
// a.js
import { value as valueB } from './b.js';
export const value = 'a' + valueB;
// b.js
export const value = 'b';
Let's say we want to move the a.js
module into a separate chunk while keeping the b.js
module in the same chunk as entry.js
. We get
import { value } from './a.js';
// b.js
const value = 'b';
// entry.js
const foo = 'foo';
console.log(value);
export { foo, value };
import { value } from './entry.js';
// a.js
export const value = 'a' + value;
You could see, to make a.js
work, we have to change the export signature of the entry chunk entry.js
and add an additional export value
. This totally violates the original intention of the code, which is to only export foo
from entry.js
.
Fortunately, Rolldown supports InputOptions.preserveEntrySignatures: 'allow-extension'
to let you inform the bundler that it is allowed to change the export signature of the entry chunk.
Enabling InputOptions.preserveEntrySignatures: 'allow-extension'
will prevent the bundler from capturing the dependencies of the captured module.