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

  1. Audit module exports for top-level function calls, DOM manipulations, or polyfill registrations.
  2. Set "sideEffects": false for pure utility libraries.
  3. 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

  1. List CSS imports explicitly in the sideEffects array for component libraries.
  2. Use conditional exports to separate browser and server entry points.
  3. Inspect bundle output with webpack-bundle-analyzer or rollup-plugin-visualizer to 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-limit

Audit protocol

  1. Run webpack-bundle-analyzer or rollup-plugin-visualizer before and after sideEffects changes to confirm the graph shrank.
  2. Compare module counts in stats.json pre/post change.
  3. 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.