Rollup Tree Shaking: How Static Analysis Shrinks Your Bundle
Rollup's tree shaking uses ES Module static structure to drop unused exports. Learn how the treeshake config tunes aggressiveness, why sideEffects flags matter, and how to verify the output in CI.
12 Apr 2026, 13:15 UTC

The bundle bloat problem
You import a utility library for one function, but the entire library — dozens of unused functions, polyfills, and side-effect-heavy initialization code — ships to every browser. That's the default behavior when a bundler can't prove what's actually needed. Rollup's tree shaking solves this by exploiting the static structure of ES Modules (import/export statements that don't change at runtime) to build a dependency graph and drop everything your entry point doesn't transitively reference.
How static analysis enables elimination
Unlike CommonJS, where require() calls can be conditional, dynamic, or constructed at runtime, ES Modules declare their dependencies up front. Rollup parses every import and export, constructs a module graph, and marks exports as "used" only when something in the entry chunk (or a dynamic import reachable from it) actually reads them. Unmarked exports are omitted from the output. This is why the research emphasizes ES Modules: they turn bundle optimization into a graph-reachability problem rather than a guessing game.
Tuning aggressiveness with the treeshake option
Rollup exposes a treeshake configuration object (or boolean) that controls three analysis passes:
- propertyReadSideEffects — assumes reading a property never has side effects unless proven otherwise
- propertyWriteSideEffects — assumes writing a property never has side effects
- tryCatchDeoptimization — preserves code inside try/catch blocks that might throw
Setting treeshake: { propertyReadSideEffects: false } tells Rollup that even reading obj.prop might trigger a getter with side effects, so it keeps more code. The default (true) is safe for most application code but can break libraries that rely on prototype getters for initialization.
Worked example: a tiny utility library
Create utils.js with three named exports:
// utils.js
export function used() { return 'kept'; }
export function unused() { return 'dropped'; }
export function sideEffect() { console.log('init'); }
Entry point main.js imports only one:
// main.js
import { used } from './utils.js';
console.log(used());
Run Rollup with npx rollup main.js --format es --output bundle.js (requires Node 18+ and Rollup installed locally or via npx). Open bundle.js — you'll see used but unused and sideEffect are gone. Now add "sideEffects": false to package.json and rebuild; the console.log in sideEffect disappears too because Rollup now treats the whole module as side-effect-free. Remove that flag and the log stays — Rollup preserves code that might mutate global state even when its return value is unused.
Where tree shaking breaks down
Three common failure modes:
- CommonJS dependencies —
require('./lib')returns an opaque object; Rollup can't safely analyze property access, so it keeps the whole module. The research flags this as "significantly harder." - Incorrect
sideEffectsflags — A package marking"sideEffects": falsewhile importing CSS or registering polyfills will silently drop those imports. Always audit third-partypackage.jsonbefore trusting the flag. - Dynamic property access — Code like
obj[methodName]()defeats static analysis; Rollup must assume every method onobjmight be called.
Verify the result in your pipeline
Add a CI step that fails if the production bundle exceeds a size budget (e.g., rollup-plugin-filesize or a custom script checking gzip size). Locally, run rollup --config --plugin=@rollup/plugin-analyzer to emit a module-size report — look for large modules that should have been shaken. If a utility library appears at full size, check whether it's CommonJS, whether sideEffects is misconfigured, or whether your code uses dynamic imports that prevent static reachability.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.