Optimizing Rollup Bundles with Tree‑Shaking and Manual Chunk Splitting
A step‑by‑step guide to configuring Rollup for reliable tree‑shaking and custom chunk splitting, including sideEffects declaration, manualChunks strategy, and verification with rollup-plugin-visualizer.
12 Oct 2025, 10:34 UTC

Desired outcome
Produce a production‑ready JavaScript bundle that is as small as possible, loads quickly, and benefits from long‑term browser caching. The guide shows how to enable reliable tree‑shaking and define custom chunk boundaries with output.manualChunks.
Prerequisites
- Node.js ≥ 18 (LTS) installed.
- Rollup ≥ 4.x available locally (
npm i -D rollup) or vianpx. - Project source written as ES modules (
import/export) – CommonJS files will limit tree‑shaking. - A
package.jsonwith asideEffectsfield (see step 3).
Procedure
1. Define entry points
In rollup.config.js set input to a string, array, or glob pattern. Globs are resolved relative to the config file.
// rollup.config.js
export default {
input: 'src/**/*.js', // auto‑includes every .js file under src/
// or explicit entries:
// input: ['src/main.js', 'src/admin.js'],
output: {
dir: 'dist',
format: 'esm',
entryFileNames: '[name]-[hash].js',
chunkFileNames: 'chunks/[name]-[hash].js',
// manualChunks defined in step 4
},
plugins: []
};
Where to run: project root (where rollup.config.js lives). No elevated permissions required.
2. Ensure tree‑shaking is active
Rollup enables tree‑shaking by default for ES modules. The only required action is to avoid patterns that defeat it:
- Do not use
require()ormodule.exportsin source files. - Avoid dynamic
import()of modules that export side‑effects unless you explicitly mark them (step 3). - If you use a minifier (e.g.,
@rollup/plugin-terser), keep it after the tree‑shaking pass – the default plugin order already does this.
3. Declare side‑effects in package.json
Tree‑shaking can only drop unused exports when it knows a module has no side‑effects. Add a sideEffects array listing files that do have side‑effects (polyfills, CSS imports, etc.). All other files are treated as pure.
// package.json
{
"name": "my-app",
"sideEffects": [
"src/polyfills.js",
"src/styles/global.css"
]
}
If the field is omitted or set to false, Rollup assumes every file may have side‑effects and will retain all exports.
4. Configure custom chunk splitting with output.manualChunks
Define a function that receives a module id and returns a chunk name. Group vendor code, heavy libraries, or feature‑specific modules to improve cache granularity.
// rollup.config.js (continued)
output: {
// …previous options
manualChunks(id) {
// id is the resolved absolute path of the module
if (id.includes('node_modules')) {
// Put each top‑level package in its own chunk
const pkg = id.split('node_modules/')[1].split('/')[0];
return `vendor-${pkg}`;
}
// Feature‑based chunks for app code
if (id.includes('/features/')) {
const feature = id.split('/features/')[1].split('/')[0];
return `feature-${feature}`;
}
// Fallback – all other app code goes to a single chunk
return 'app';
}
}
Risk: Over‑splitting creates many tiny chunks, increasing HTTP/2 or HTTP/1.1 round‑trips. Start with the vendor‑per‑package strategy and measure.
5. Add a visualizer for verification
Install rollup-plugin-visualizer (dev dependency) and enable it only for analysis builds.
npm i -D rollup-plugin-visualizer
// rollup.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
// …input, output
plugins: [
process.env.ANALYZE && visualizer({
filename: 'bundle-analysis.html',
open: false,
gzipSize: true
})
].filter(Boolean)
};
Run the analysis build:
ANALYZE=1 npx rollup --config rollup.config.js
Open bundle-analysis.html in a browser to inspect chunk sizes, module composition, and whether unused exports were removed.
Expected checks
- Bundle size reduction – Compare
dist/total size before and after enablingsideEffectsandmanualChunks. A 10‑30 % drop is typical for medium‑sized apps. - Unused export removal – In the visualizer, modules that only export unused functions should show zero bytes in the final chunks.
- Chunk count – Aim for <10‑15 chunks for a typical SPA. If you see >30, consolidate
manualChunkslogic (e.g., group low‑traffic vendors together). - Cacheability – Verify that vendor chunk hashes change only when the corresponding
package.jsondependency version changes.
Recovery / rollback options
If a production build fails or behaves unexpectedly:
- Disable manualChunks – Comment out the
manualChunksfunction; Rollup falls back to a single chunk per entry point. - Re‑enable side‑effects conservatively – Set
"sideEffects": trueinpackage.jsonto keep all code, then iteratively mark files as pure. - Revert minifier – Remove
@rollup/plugin-tersertemporarily to isolate whether minification introduced a bug.
All of these changes are configuration‑only; no file system state is mutated beyond the dist/ output directory, so a simple rm -rf dist && npm run build restores a clean state.
Limitations & practical verification
- Tree‑shaking cannot eliminate code reachable via dynamic
import()unless the imported module is also marked side‑effect‑free and the bundler can prove the import is never executed. - CommonJS dependencies (e.g., older lodash builds) are treated as having side‑effects; consider ES‑module alternatives (
lodash-es) or use@rollup/plugin-commonjswithnamedExportsto improve shaking. - The visualizer reports gzipped sizes only when
gzipSize: trueis set; ensure your server actually serves gzip/ Brotli for accurate comparison.
To verify the final result in CI, add a script that fails if the total gzipped size exceeds a threshold:
# package.json
"scripts": {
"build": "rollup --config rollup.config.js",
"size-check": "ANALYZE=1 rollup --config rollup.config.js && node scripts/check-bundle-size.js"
}
check-bundle-size.js can parse bundle-analysis.html or the generated .map files and exit non‑zero when limits are crossed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.