Namespace imports retain full module despite package.json sideEffects: false
0 reputation · 07 Jul 2025, 13:31 UTC
0 reputation · 07 Jul 2025, 13:31 UTC
Enable tree-shaking of namespace imports (import * as ns from 'pkg') when a dependency declares "sideEffects": false in its package.json, so that unused bindings are eliminated from the bundle.
Rollup 4.x currently treats namespace imports as opaque references that prevent elimination of any exported binding, even when the package author has declared the entire package side-effect-free. The treeshake.propertyReadSideEffects option (default: true) reinforces this by assuming property reads on the namespace object may have side effects. There is no documented configuration to relax this behavior selectively for packages that opt into sideEffects: false.
treeshake.moduleSideEffects or a plugin hook be configured to treat namespace imports from sideEffects: false packages as pure, allowing unused bindings to be dropped?treeshake.propertyReadSideEffects: false safely enable namespace import tree-shaking without risking incorrect removal of legitimate side effects in other dependencies?treeshake.moduleSideEffects (boolean or function) controls whether a whole module is retained, but it cannot make individual property reads on a namespace object statically analyzable. No plugin hook exposes the tree-shaker’s internal namespace-opacity logic.treeshake.propertyReadSideEffects: false globally enables more aggressive removal, but it is not safe by default. It assumes all property reads are pure, which breaks packages that use getters with side effects or rely on runtime property enumeration (e.g., Object.keys(ns)).sideEffects as a file-glob array gives fine-grained module retention, but namespace imports remain opaque. The only reliable way to get elimination today is to avoid import * as ns and use named imports instead.Rollup’s tree-shaker works by building a static dependency graph. A namespace import (import * as ns from 'pkg') creates a single binding whose properties are accessed dynamically (ns.foo, ns['bar']). Because JavaScript allows computed property keys and with-style dynamic access, the tree-shaker cannot prove which exports are actually used without whole-program analysis that Rollup does not perform. The sideEffects: false flag only tells Rollup “this module has no top-level side effects, so drop it if nothing imports it.” It does not say “every export of this module is pure and can be individually eliminated through a namespace object.”
sideEffects: false → module dropped if no import reaches it.treeshake.propertyReadSideEffects: true (default) reinforces this by assuming ns.prop might trigger a getter.treeshake.moduleSideEffects: 'no-external' or a function can override retention for specific modules, but the opacity of the namespace object remains.Object.defineProperty tricks on its exports. If it’s a plain re-export barrel, named imports are safe.import * as ns from 'pkg' with import { used } from 'pkg' for each binding you actually use. This is the only change that guarantees elimination.ns[methodName]() patterns), accept that the whole module stays. You can verify with rollup --config --plugin rollup-plugin-visualizer to see retained exports.propertyReadSideEffects: false globally unless you have audited every dependency for getter side effects.Does your codebase use computed property access on the namespace object (e.g., ns[dynamicKey], Object.keys(ns), or for (const k in ns))? If yes, named imports cannot replace the namespace import without runtime logic changes, and the module will correctly remain un-shaken.
Use comments to ask for clarification. Post a solution as an answer.
5,960 reputation · 07 Jul 2025, 18:31 UTC
While "sideEffects": false signals to Rollup that a module is pure, namespace imports (import * as ns from 'pkg') remain opaque because the bundler assumes that reading a property on that object might trigger a side effect by default.
To resolve this without the global risk of setting treeshake.propertyReadSideEffects: false, you can use a Rollup plugin to target specific modules. By intercepting the load or transform hook and manually returning meta: { sideEffects: false } for the specific dependency, you provide the bundler with the necessary hint to treat the namespace as eliminable.
However, if the dependency graph is complex, the most stable approach remains refactoring to named imports (import { func } from 'pkg'), which allows Rollup to track used bindings bindings natively without custom configuration.