Configuring sideEffects for Optimal Tree-Shaking
The sideEffects declaration in package.json establishes a critical purity contract between library authors and modern bundlers. Within the broader architecture of Advanced Tree-Shaking & Dependency Optimization, correctly signaling module purity enables static analyzers to safely prune unused exports without breaking runtime initialization sequences or global state mutations. Misconfiguration introduces two failure modes: false positives (a module that truly has side effects is dropped, causing runtime failures) and false negatives (a pure module is retained unnecessarily, bloating payloads by 8–12%).
The Static Analysis Contract: AST Traversal and Purity Inference
Modern bundlers parse Abstract Syntax Trees (AST) to map import/export relationships prior to code generation. When sideEffects is omitted from package.json, the bundler assumes the worst-case: every file may mutate global state, so no file is safe to drop. Explicit declarations allow the optimizer to bypass that conservative path.
CommonJS require() calls inherently break static purity guarantees because the dependency path may be computed at runtime. Converting CJS Libraries to ESM for Better Bundling covers the migration to static import syntax that restores analyzer visibility.
Implementation protocol
- Audit module exports for top-level function calls, DOM manipulations, or polyfill registrations.
- Set
"sideEffects": falsefor pure utility libraries. - Use an explicit glob array for initialization scripts, CSS imports, and polyfill files.
{
"name": "@scope/utilities",
"version": "2.1.0",
"sideEffects": [
"*.css",
"./lib/polyfills/*.js",
"./lib/init-runtime.js"
]
}Strict glob patterns reduce dependency graph edges by 15–30% in large monorepos, preventing unnecessary vendor chunk splitting. Build times may drop by ~18%, but CI smoke tests are mandatory to catch any missing polyfill or global registration that was silently pruned.
Chunk Graph Behavior & AST Traversal Mechanics
During the bundling phase, the optimizer traverses the module graph from entry points outward. Pure modules are marked for elimination before scope hoisting occurs. This process directly complements Eliminating Dead Code with Modern Build Tools by ensuring unused exports are stripped prior to minification rather than relying on post-build dead code elimination.
Webpack 5 (webpack.config.js)
module.exports = {
mode: 'production',
optimization: {
sideEffects: true, // Respect package.json sideEffects field
usedExports: true,
concatenateModules: true, // Scope hoisting
moduleIds: 'deterministic',
chunkIds: 'deterministic'
},
stats: {
modules: true,
reasons: true,
optimizationBailout: true // Shows why a module was kept
}
};Vite 5 (vite.config.ts)
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
treeshake: {
moduleSideEffects: 'no-external', // Treat external packages as pure unless declared otherwise
propertyReadSideEffects: false
},
output: {
chunkFileNames: 'assets/[name]-[hash].js'
}
}
}
});Aggressive pruning reduces chunk count variance and stabilizes long-term caching hashes. Deeper AST analysis adds roughly 5% to cold-start compilation time but yields 12–18% smaller production payloads.
Framework-Specific Workflows & Integration Patterns
Component frameworks require careful side-effect mapping due to lifecycle hooks, context providers, and global CSS injection. React libraries that bundle CSS-in-JS or register providers at import time need explicit overrides. Vue SFCs compile to pure JS but may inject global styles via the framework’s style injection.
The key is separating entry points by environment using conditional exports:
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./components/*": {
"import": "./dist/components/*.mjs",
"types": "./dist/components/*.d.ts"
}
}
}Integration protocol
- List CSS imports explicitly in the
sideEffectsarray for component libraries. - Use conditional exports to separate browser and server entry points.
- Inspect bundle output with
webpack-bundle-analyzerorrollup-plugin-visualizerto verify that each import path resolves to the correct chunk.
Proper export mapping prevents framework runtime duplication across micro-frontends and isolates hydration scripts into async chunks, improving Time to Interactive (TTI) by 150–300 ms on mobile. The trade-off is increased maintenance overhead for export mapping as the library evolves.
Measurable Trade-offs & Audit Workflows
Static analysis alone cannot guarantee runtime safety, especially when third-party packages have incorrect or missing sideEffects declarations. How to audit sideEffects in large npm packages provides a systematic approach for verifying module purity in dependencies you do not control.
For your own codebase, a CI gate that builds, measures, and fails on regression is the minimum viable safeguard:
name: Bundle Integrity Gate
on: [pull_request]
jobs:
audit-side-effects:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- name: Build and check size
run: |
npm run build
npx size-limitAudit protocol
- Run
webpack-bundle-analyzerorrollup-plugin-visualizerbefore and aftersideEffectschanges to confirm the graph shrank. - Compare module counts in
stats.jsonpre/post change. - Execute runtime smoke tests to catch missing polyfills or global registrations.
Establishing this baseline stabilizes chunk sizes across releases, reduces cache invalidation rates by ~40%, and prevents bundle bloat regressions from accumulating silently over time.