Rollup manualChunks: Splitting Vendor Code for Real Caching Wins
Learn how Rollup's manualChunks option separates vendor libraries from application code, improving browser cache reuse and reducing re-downloads on every deploy.
12 May 2026, 08:09 UTC

You push a one-line bug fix to your JavaScript bundle. The file hash changes, so every returning visitor re-downloads the entire payload—including React, lodash, and whatever else lives in node_modules. The application logic changed by a few hundred bytes, but the network tab shows a full re-download. That is the problem vendor chunking solves, and Rollup's manualChunks option is one of the most direct ways to fix it.
The caching problem with monolithic bundles
Browsers cache files by URL. When your build tool emits a single main.js that contains both application code and third-party dependencies, any source change produces a new file. The browser cannot tell that 95 percent of that file is unchanged vendor code, so it fetches the whole thing again.
Vendor code—the libraries you install from npm—tends to change far less often than your own source. If you can physically separate those dependencies into their own output file, a source-only deploy leaves the vendor file untouched. The browser reuses its cached copy, and the user downloads only the changed application chunk.
How manualChunks works
Rollup's output.manualChunks option accepts a function. During the build, Rollup calls this function for every module it processes. The function receives the module's id—an absolute file path on disk—and returns either a string (the name of the chunk that module should join) or undefined (meaning "leave this in the default entry chunk").
Because the decision happens at build time, you have full control. You can group by directory, by package name, or by any heuristic you can express in JavaScript. Rollup still respects ES module semantics inside each chunk, so tree-shaking—the process of removing unused exports—continues to work within the vendor bundle.
A worked example
Suppose you have a standard Rollup config and you want to pull everything from node_modules into a shared vendor chunk. In your project root, edit rollup.config.js:
// rollup.config.js
export default {
input: 'src/main.js',
output: {
dir: 'dist',
format: 'es',
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor';
}
}
}
};
Run the build from your project root with:
npx rollup -c
No special permissions are required beyond normal filesystem access to write the dist directory. After the build completes, inspect dist/. You should see main.js and vendor.js. Open vendor.js and confirm it contains only modules resolved from node_modules—libraries like React or lodash—and none of your application source files.
To verify the caching behavior, open your application in a browser with the developer tools Network tab enabled. Make a trivial change to a file under src/, rebuild, and reload the page. You should observe that main.js is fetched anew while vendor.js is served from cache. If both files are re-downloaded, your server or build pipeline is likely changing the vendor filename on every build, which defeats the purpose.
The trade-off: broad rules are easy, good rules are not
The example above is simple, but it is also blunt. Returning 'vendor' for every node_modules path lumps every dependency into one file. That works well for a small dependency tree, but it has real costs:
- Cache granularity suffers. If you upgrade one library, the entire vendor chunk changes. A more granular strategy—returning different chunk names for different packages—lets unchanged libraries stay cached.
- New dependencies need rule updates. If you add a large library and your
manualChunksfunction does not account for it, that code lands inmain.jsand bloats your frequently changing bundle. - Duplication risk. With multiple entry points, an overly broad rule can pull the same module into a shared chunk while also inlining it elsewhere, increasing total payload size.
A more targeted function might inspect the module path and return a chunk name based on the top-level package:
manualChunks(id) {
if (id.includes('node_modules')) {
// Group by top-level package name to keep chunks granular
const match = id.match(/node_modules\/((?:@[^/]+\/)?[^/]+)/);
if (match) return match[1];
}
}
This produces separate chunks for each top-level dependency. The trade-off is more HTTP requests, which matters on high-latency networks. There is no universal correct answer; the right granularity depends on how often each dependency changes relative to your deploy cadence.
When to reach for it
Manual chunking is worth the configuration effort when your vendor code is stable, your application code changes frequently, and you control the build pipeline. If your dependencies change on every deploy, or if you are already using a framework with sophisticated code-splitting defaults, the benefit shrinks.
Start with the broad node_modules check, confirm that vendor.js is stable across source-only rebuilds, and then decide whether finer-grained chunking actually improves your cache hit rate. The Network tab is your ground truth: if vendor.js is not being reused, the rule needs adjustment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.