How to audit sideEffects in large npm packages

Diagnosing Unexplained Bundle Inflation and Tree-Shaking Failures

Large npm packages frequently trigger unexpected chunk inflation when bundlers like Webpack 5 or Vite 5 retain modules marked as having side effects. The primary symptom is a 15–40% increase in final JavaScript payload despite importing only a single utility function. Build logs may surface warnings about unoptimized chunks, and runtime analysis reveals that unused modules are eagerly evaluated because of conservative sideEffects: true defaults or missing declarations in upstream package.json manifests.

Execution workflow

  1. Generate a deterministic build manifest:
    # Webpack
    npx webpack --mode=production --json=compilation-stats.json
    
    # Vite
    npx vite build
  2. Filter the Webpack stats output for modules that are flagged as having side effects:
    jq '.modules[] | select(.sideEffects == true or .sideEffects == null) | {path: .name, size: .size}' compilation-stats.json
  3. Cross-reference the retained modules against your actual import statements to isolate false-positive inclusions.

Tracing Implicit Global Mutations and Barrel File Interop

The root cause typically stems from implicit side effects hidden within CommonJS-to-ESM transpilation layers, global polyfills, or barrel exports that mask unused code paths. When a package lacks explicit sideEffects: false, modern bundlers must conservatively assume every file modifies global state. This behavior is described in Configuring sideEffects for Optimal Tree-Shaking, where implicit mutations in initialization blocks or prototype extensions force the bundler to retain entire dependency trees. Legacy barrel files (index.js re-exporting all modules) prevent static analysis from pruning dead branches, creating a false-positive retention cascade.

Execution workflow

  1. Inspect the upstream package manifest for missing or boolean sideEffects keys:
    cat node_modules/<package>/package.json | jq '.sideEffects'
  2. Use ESLint to scan for top-level global mutations in the package source:
    npx eslint@8 --no-eslintrc \
      --parser-options=ecmaVersion:2022 \
      --rule '{"no-implicit-globals": "error"}' \
      'node_modules/<package>/src/**/*.js'
  3. Check for circular dependencies that might force full-module inclusion:
    npx madge --circular --extensions js,ts node_modules/<package>/src

Implementing Automated sideEffects Auditing and Patch Workflows

To resolve retention issues, apply targeted overrides via bundler configuration, or use patch-package to inject precise glob patterns directly into node_modules/<package>/package.json for packages that lack proper declarations.

Execution workflow & config patches

  1. Create a patch for a specific package (replace my-package with the actual package name):

    # Edit node_modules/my-package/package.json to add the sideEffects field, then run:
    npx patch-package my-package
  2. The generated patch in patches/my-package+1.0.0.patch will include the sideEffects addition. When applied, the relevant section of the package’s package.json will look like:

    {
      "sideEffects": [
        "**/*.css",
        "**/init.js",
        "**/polyfills/*.js"
      ]
    }
  3. Enforce overrides at the bundler level for packages you cannot patch:

    Webpack (webpack.config.js)

    module.exports = {
      optimization: {
        usedExports: true,
        sideEffects: true,
        moduleIds: 'deterministic'
      },
      module: {
        rules: [
          {
            // Fallback: treat a specific package as side-effect-free
            test: /node_modules\/<package>/,
            sideEffects: false
          }
        ]
      }
    };

    Vite (vite.config.ts)

    import { defineConfig } from 'vite';
    
    export default defineConfig({
      build: {
        rollupOptions: {
          treeshake: {
            moduleSideEffects: 'no-external'
          }
        }
      },
      optimizeDeps: {
        include: ['<package>']
      }
    });
  4. Fallback: If overrides break runtime execution (missing polyfill, unregistered provider), revert and explicitly import the required initialization file at the application entry point: import '<package>/init';. This makes the dependency explicit and visible to the bundler without silencing side effects globally.

Quantifying Delta Reduction and Runtime Import Validation

Post-implementation verification requires strict metric tracking to confirm that the audit eliminated dead code without introducing runtime regressions.

Execution workflow

  1. Measure size delta against a stored baseline:
    npx size-limit --compare
  2. Verify module-level changes in the Webpack stats JSON:
    jq '.modules[] | select(.name | test("<package>")) | {path: .name, sideEffects: .sideEffects, usedExports: .usedExports}' compilation-stats.json
  3. Run the integration test suite to catch missing polyfill or initialization regressions:
    npm run test:integration -- --grep "polyfill|init"
  4. Commit size-limit thresholds to the repository for automated PR gating:
    {
      "size-limit": [
        {
          "path": "dist/*.js",
          "limit": "150 KB",
          "running": false
        }
      ]
    }

The target for a successful audit is a minimum 20% reduction in uncompressed JavaScript size for the affected package, with zero increase in runtime error rate. Use webpack-bundle-analyzer or rollup-plugin-visualizer to confirm that unused exports are now excluded from the final chunk, and integrate these checks into CI to prevent future sideEffects misconfigurations.