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
- Generate a deterministic build manifest:
# Webpack npx webpack --mode=production --json=compilation-stats.json # Vite npx vite build - 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 - 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
- Inspect the upstream package manifest for missing or boolean
sideEffectskeys:cat node_modules/<package>/package.json | jq '.sideEffects' - 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' - 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
-
Create a patch for a specific package (replace
my-packagewith the actual package name):# Edit node_modules/my-package/package.json to add the sideEffects field, then run: npx patch-package my-package -
The generated patch in
patches/my-package+1.0.0.patchwill include thesideEffectsaddition. When applied, the relevant section of the package’spackage.jsonwill look like:{ "sideEffects": [ "**/*.css", "**/init.js", "**/polyfills/*.js" ] } -
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>'] } }); -
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
- Measure size delta against a stored baseline:
npx size-limit --compare - Verify module-level changes in the Webpack stats JSON:
jq '.modules[] | select(.name | test("<package>")) | {path: .name, sideEffects: .sideEffects, usedExports: .usedExports}' compilation-stats.json - Run the integration test suite to catch missing polyfill or initialization regressions:
npm run test:integration -- --grep "polyfill|init" - Commit
size-limitthresholds 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.