Using Rollup Tree‑Shaking to Remove Unused JavaScript Code
Learn how to configure Rollup’s built‑in tree‑shaking to drop dead exports, understand its limits, and verify that the bundle really shrinks.
22 Oct 2025, 23:55 UTC

Problem: Bundles Contain Code You Never Use
When you build a frontend library or application with Rollup, the output often includes helper functions, utilities, or lodash methods that are never referenced in your code. This dead weight increases download size, parse time, and runtime memory usage, especially on slow networks.
Takeaway
Enable Rollup’s native tree‑shaking by authoring code as ES modules, setting treeshake: true (or an options object), and providing side‑effect information. After the build, inspect the bundle to confirm that unused exports are removed.
Requirements for Effective Tree‑Shaking
- ES module syntax: Source files must use static
importandexportstatements. Dynamicimport()calls or CommonJSrequireprevent static analysis. - Side‑effect awareness: Rollup assumes a module may have side effects unless told otherwise. If a module is marked as side‑effect free, the bundler can safely drop unused exports.
- Preserve ES module shape through plugins: Any transform plugin (Babel, TypeScript, etc.) must run before Rollup’s tree‑shaking stage or be configured to keep
import/exportstatements; otherwise the analyzer sees CommonJS output and conservatively keeps everything.
Smallest Suitable Design
The minimal configuration that activates tree‑shaking for a plain ES‑module project is:
// rollup.config.js
import { terser } from 'rollup-plugin-terser';
export default {
input: 'src/main.js',
output: {
file: 'dist/bundle.js',
format: 'es',
sourcemap: true,
},
treeshake: true, // enables the built‑in pass
plugins: [terser()],
};
No extra plugins are required for basic tree‑shaking; the flag tells Rollup to run its dead‑code elimination after the module graph is resolved.
Trust and Data Boundaries
Tree‑shaking is a static guarantee: Rollup can prove that an export is unused only if it can also prove that the module has no observable side effects. The trust boundary lies where Rollup hands control to external resolvers or plugins:
- If a dependency is resolved as a CommonJS package, Rollup treats the whole file as potentially side‑effectful and keeps it.
- If a plugin transforms ES modules to CommonJS before tree‑shaking, the analysis sees a side‑effectful module and cannot drop exports.
- If you explicitly mark a module as side‑effect free (via
sideEffects: falseinpackage.json or a/* @__PURE__ */comment), Rollup trusts that claim and may safely discard unused exports.
Operational Checks and Failure Modes
After a build, verify that the expected dead code is absent. A simple check is to grep for a known unused identifier:
# After running rollup -c
grep -n 'unusedHelper' dist/bundle.js
# Expected output: nothing (exit code 1)
If the identifier appears, consider these common causes:
- False‑positive side‑effect detection: The module (or one of its imports) lacks a side‑effect hint, so Rollup keeps it.
- Dynamic import patterns: Code like
import(`./${name}.js`)hides the exact module from static analysis, forcing Rollup to retain the whole chunk. - Plugin‑induced AST mutation: A plugin that runs after tree‑shaking or that rewrites
importtorequirecan obscure the analyzer’s view.
Conditions That Would Change the Design
You would revisit the tree‑shaking approach if any of the following become true in your project:
- You rely heavily on Dynamic
import()for code‑splitting; tree‑shaking still works on the static imports, but the dynamic chunks may need separate side‑effect flags. - You consume a large library that only provides a CommonJS build (e.g., older versions of Lodash). In that case, either switch to the ES module build (
lodash-es) or manually mark the library as side‑effect free if you know it is safe. - You introduce a plugin that must run after tree‑shaking (e.g., a license‑banner injector). Ensure the plugin does not modify the import/export tree; otherwise move it before the tree‑shaking pass or disable tree‑shaking for that plugin.
Concrete Example
Create a tiny project to see the effect:
// src/utils.js
export function used() {
return 'hello';
}
export function unused() {
return 'dead';
}
// src/main.js
import { used } from './utils.js';
console.log(used());
// package.json (excerpt)
{
"name": "tree-shake-demo",
"version": "1.0.0",
"sideEffects": false, // tells Rollup all modules are side‑effect free unless overridden
"dependencies": {}
}
Run Rollup with the configuration shown above. After the build, inspect the bundle:
# Look for the unused function
if grep -q 'unused' dist/bundle.js; then
echo 'Unused code present – tree‑shaking failed'
else
echo 'Unused code removed – tree‑shaking succeeded'
fi
If the output reports removal, the tree‑shaking pass succeeded. If not, re‑examine the module for hidden side effects (e.g., a top‑level console.log in utils.js) or check that no plugin transformed the ES modules to CommonJS before the analyser ran.
Limitations and Practical Verification
Tree‑shaking cannot eliminate:
- Code accessed via
evalornew Functionwith strings built at runtime. - Imports whose specifier is a variable or computed expression (
import mod from './' + name). - Modules that deliberately export side‑effectful code (e.g., a polyfill that mutates
Array.prototype).
To verify that a particular piece of code is truly dead, you can:
- Add a unique comment or console message inside the suspect function.
- Build the bundle.
- Search the output for that comment or string. Absence indicates the function was dropped.
Remember that the check is only as reliable as the static analysis; if the build system later changes (new plugin, different Rollup version), re‑run the verification.
Summary
Rollup’s tree‑shaking is a powerful, zero‑plugin optimisation when you author ES modules and provide correct side‑effect information. The smallest effective design is simply setting treeshake: true in your config. Trust ends at the point where external resolvers or plugins may obscure the module graph, so keep transforms ES‑module‑aware and mark dependencies as side‑effect free when safe. After each build, grep for known dead symbols or insert a unique marker to confirm the optimisation worked, and be aware of the patterns that defeat static analysis.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.