Rollup 4.x sideEffects: false package declaration vs unannotated module side effects
0 reputation · 03 Mar 2023, 06:23 UTC
0 reputation · 03 Mar 2023, 06:23 UTC
Rollup 4.x treats "sideEffects": false in package.json as a strong signal to eliminate unused exports, yet individual modules may contain top-level statements (such as console.log or mutation of global state) that are not annotated with /* @__PURE__ */. The bundler must decide whether to trust the package-level declaration or require per-module purity annotations.
Version 4 tightened side-effect detection for re-exported namespace objects, potentially breaking builds that relied on looser elimination in 3.x. The webpack-style array syntax "sideEffects": ["*.css"] is ignored; only boolean false is recognized. Monorepo setups with hoisted node_modules may cause Rollup to read package.json from an unexpected location, altering side-effect assumptions. The treeshake.moduleSideEffects option ("no-external" | "if-removed" | false) changes whether external dependencies are considered side-effect-free, with "if-removed" as the default.
Source maps do not record which modules were eliminated, making post-build verification indirect. Watch mode reuses the module graph from the previous build; if package.json sideEffects changes between runs, the cached graph may retain stale assumptions until a full restart.
Does Rollup 4.x prioritize the package.json boolean over top-level statements in individual modules when both are present? How does the treeshake.moduleSideEffects option interact with a dependency that declares sideEffects: false but contains unannotated side effects? What verification approach reliably confirms which modules were eliminated without relying on source maps?
Rollup 4.x does not let a package-level "sideEffects": false suppress top-level statements in modules that are actually imported. The flag only tells the treeshaker that unused exports can be dropped safely. If a module is kept because at least one export is used, its top-level code (console.log, global mutations, etc.) still runs.
The treeshake.moduleSideEffects option changes how external packages are treated. With the default "if-removed", a dependency that declares sideEffects: false but contains unannotated side effects will be preserved—and those side effects will execute—as soon as any export is retained. Setting moduleSideEffects: false forces Rollup to treat all externals as side-effect-free, which can incorrectly eliminate modules that do have real side effects.
To verify which modules were eliminated without relying on source maps, use a build-time plugin that logs the final module graph (e.g., a buildEnd hook writing this.getModuleIds() to a file) or run rollup-plugin-visualizer and inspect the generated tree.
sideEffects: false/* @__PURE__ */ on individual expressions give finer-grained control but are not required for the package flag to work.treeshake.moduleSideEffects settings| Value | Behavior for external dependency with sideEffects: false but real side effects |
|---|---|
"if-removed" (default) | Keeps the module if any export is used; side effects execute. Drops it only when all exports are removed. |
false | Treats all externals as side-effect-free, ignoring the package flag. Risk: modules with real side effects may be incorrectly eliminated. |
"no-external" | Only affects whether external modules are considered for treeshaking at all; similar to "if-removed" for side-effect decisions. |
buildEnd hook// rollup.config.js
import { writeFileSync } from 'fs';
export default {
plugins: [{
name: 'module-audit',
buildEnd() {
const ids = this.getModuleIds();
writeFileSync('module-graph.json', JSON.stringify(ids, null, 2));
}
}]
};
Run a clean build (rm -rf node_modules/.cache && rollup -c) and compare module-graph.json against the expected module list.
rollup-plugin-visualizernpm i -D rollup-plugin-visualizer
# add to plugins: visualizer({ open: true })
Modules absent from the generated treemap were eliminated.
Add a unique console.log('MODULE_X_LOADED') to a module you expect to be dropped. Run the bundle; if the log never appears, the module was removed.
"sideEffects": ["*.css"]; only boolean false is respected.package.json sideEffects changes, restart the watch process.node_modules may cause Rollup to read a different package.json, altering side-effect assumptions.What value is currently configured for treeshake.moduleSideEffects in your Rollup config? The recommendation changes if you have overridden the default "if-removed".
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 03 Mar 2023, 11:46 UTC
In Rollup 4.x, "sideEffects": false in a package’s package.json is treated as a blanket promise that *none* of the package’s modules perform side effects. However, if a module contains unannotated top‑level statements (e.g., console.log or global mutation), Rollup will **not** drop that module even when the package declares sideEffects:false. Marking the module with /* @__PURE__ */ or removing the side‑effecting code is necessary for elimination.
The treeshake.moduleSideEffects option controls how external dependencies are handled:
false – assume all externals are side‑effect‑free.no-external – keep the default package‑level logic for deps.if-removed (default) – treat an external as side‑effect‑free only if it is removed from the final bundle.--stats to inspect the treeshake section, or use rollup-plugin-visualizer to see which modules survive.