Rollup Tree-Shaking and the sideEffects Field: An Architecture Note
How Rollup decides which modules to drop, why package.json sideEffects is a trust boundary, and the checks that catch a wrong declaration before users do.
03 Sept 2026, 05:30 UTC

The decision: who declares what can be dropped
Rollup builds a dependency graph from ES module import and export statements, marks exports reachable from the entry point, and removes the rest. Removal is gated by side-effect analysis: if a module might do something when evaluated — write to disk, mutate a global, register a polyfill — Rollup keeps it even when its exports look unused.
The sideEffects field in package.json is how a package author tells Rollup which modules are safe to drop. That makes it a trust boundary between the package author and every consumer's build, not a local build setting.
Requirements for a tree-shakeable library
- Source and published output use ES module syntax, not CommonJS.
- Modules you want dropped have no top-level side effects.
package.jsondeclaressideEffectsaccurately.- An
exportsfield points consumers at the ESM build.
The smallest suitable design
For a library with no top-level side effects, the smallest correct declaration is a single boolean:
{
"name": "example-lib",
"version": "1.0.0",
"type": "module",
"sideEffects": false,
"exports": {
".": "./dist/index.js"
}
}
Consumers need no Rollup configuration for this to work; tree-shaking is on by default when the input is ESM. The author's job is to make false true.
If the package mixes pure code with files that must always run — CSS imports, a polyfill entry, a global registration — use a per-file array instead:
"sideEffects": ["*.css", "src/polyfill.js"]
Rollup treats listed files as side-effecting even when their imports appear unused, and preserves their execution order relative to other side-effecting modules.
The trust boundary in practice
The sideEffects field is not standardized by any specification. It is a de facto convention adopted by Rollup, webpack, and terser. Some packages omit it; some use values outside the common forms. When it is absent or true, Rollup is conservative: if any export from a module is used, the whole module is retained. When it is false, Rollup trusts the author and drops unused exports and their module code.
Two layers are involved and they are easy to confuse:
| Layer | Who sets it | Effect |
|---|---|---|
sideEffects in package.json | Package author | Declares which modules can be dropped |
treeshake.moduleSideEffects in rollup.config.js | Consumer | Overrides behavior, including for external dependencies |
A consumer override such as moduleSideEffects: 'no-external' tells Rollup to treat external dependencies as side-effect-free. It is a blunt instrument: it can drop a dependency's polyfill just as easily as it drops dead code.
Operational checks before publishing
Run these in the package repository with normal user permissions. None require elevated access.
- Minimal drop test. Create two modules: one with an export the entry uses, one with an export nothing uses. Add a top-level
console.logto the unused module. Build with Rollup and search the output for the log and the unused export name. - Debug the tree-shake decision.
DEBUG=rollup:treeshake npx rollup -cprints which modules Rollup marked side-effect-free and which exports it removed. The debug namespace can differ between Rollup versions; confirm it against the version you run. - Inspect plugin output. If Babel or TypeScript runs before Rollup, check the transformed code still uses
import/export. A plugin emitting CommonJS breaks the static analysis and silently disables tree-shaking. - Grep the bundle. Search the generated file for a string unique to the code you expected to be dropped. Absence is the pass condition.
Expected result for step 1: with "sideEffects": false, the top-level log is absent from the bundle. If that log represented real work, the declaration removed a real side effect. With "sideEffects": true or the field omitted, the log is retained — correct but larger.
Failure modes and how they appear
- False declaration, real side effect. A dependency declares
"sideEffects": falsebut mutates a global prototype at import time. Rollup drops the import. The build succeeds; the app fails at runtime with no build-time warning. This is a supply-chain trust issue, not a bundler bug. - Dropped styles. A package with CSS imports and
"sideEffects": falseloses its stylesheets. List the CSS patterns in the array form instead. - CommonJS dependencies. Tree-shaking degrades significantly when a dependency cannot be converted to ESM. The bundle grows; nothing breaks.
- Dynamic import boundaries.
import()creates separate chunks by default, and cross-chunk elimination is limited. Unused exports in a dynamic chunk are dropped only if the chunk is never loaded. - Plugin-injected helpers. Transpilation plugins that inject runtime helpers can introduce side effects that defeat tree-shaking if not configured carefully.
Version assumptions to verify
Rollup 4.x reportedly changed the default for moduleSideEffects on external dependencies compared with 3.x. Treat that as a claim to check against the release notes for the exact version in your lockfile: an existing config may produce a larger or smaller bundle after an upgrade without any config change. The propertyReadSideEffects: false option allows dropping unused property reads on external namespace objects; it can also drop reads that would have thrown, so enable it only after testing.
Conditions that would change the design
- If any dependency is CommonJS-only, expect weaker elimination and consider whether the size gain justifies finding ESM alternatives.
- If execution order of side-effecting modules must survive across dynamic import boundaries, manual chunking or
preserveEntrySignaturesmay be needed. - If you cannot guarantee no top-level side effects, use the per-file array rather than
false. A narrow declaration that is correct beats a broad one that is fast. - If a consumer overrides
moduleSideEffectsglobally, your accurate declaration may be ignored. Document the requirement in the package README.
Verification checklist
- Build the package and confirm unused exports are absent from the output.
- Confirm every file listed in
sideEffectsactually needs to run. - Confirm the published entry resolves to ESM through the
exportsfield. - Re-run the minimal drop test after any Rollup or plugin upgrade.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.