Back to the blog
Webpack· Jun 24, 2026· 7 min read

Tree shaking in Webpack: why it silently fails, and how to verify it's working

ES module static analysis, the sideEffects field, and the three most common reasons dead code ends up in your bundle anyway despite tree shaking being 'on'.

#webpack#tree-shaking#performance#esm

Tree shaking's promise is that unused exports get eliminated from your bundle. In practice, it silently fails more often than it silently succeeds, because it depends on Webpack being able to statically prove a piece of code is unreachable - and several extremely common patterns break that proof without throwing any error.

Why it needs ES modules, specifically

Tree shaking works by static analysis of `import`/`export` syntax, which has a fixed shape the bundler can reason about without running any code. CommonJS's `require()`/`module.exports` can be called conditionally, dynamically, with a computed path - none of that is statically analyzable, so Webpack has to assume every export might be used. A package that ships CommonJS, even with a modern-looking API, cannot be tree-shaken no matter how your config is set.

The `sideEffects` field: telling Webpack it's safe to delete unused files

{
  "name": "@org/ui-kit",
  "sideEffects": false
}

Even with ESM syntax, Webpack conservatively keeps a module if importing it could run a side effect - a polyfill, a CSS injection, a global event listener registration. `sideEffects: false` in package.json is a promise from the package author that no module in the package does anything observable just from being imported, which lets Webpack drop entire unused files, not just unused named exports within a file.

{
  "sideEffects": ["./src/polyfills.js", "*.css"]
}

For packages that DO have real side-effectful files (a polyfill entry, CSS files that must be injected), the array form marks exactly which files to exempt from the aggressive shaking - getting this list wrong in either direction either keeps dead code or drops code you actually needed.

The three silent-failure patterns we see most

  • A re-export barrel file (index.ts that does `export * from './Button'`) forces Webpack to keep everything reachable through the barrel unless every consumer imports directly from the specific file - barrels are convenient and quietly defeat tree shaking at scale.
  • Class methods and object properties aren't tree-shaken even when unused - only top-level ESM exports are. An unused method on an otherwise-used class ships regardless.
  • Babel's `@babel/preset-env` compiling ESM to CommonJS before Webpack sees it (a common misconfiguration when a package's build step runs Babel with default settings) silently disables shaking for that entire package - check the actual published output, not just the source.

Verifying it's actually working

Don't trust that tree shaking is happening just because you didn't get an error - use `webpack-bundle-analyzer` to visually confirm a function you know is unused isn't in the output, or set `optimization.usedExports: true` with `mode: 'production'` and grep the bundle for a distinctive unused-export's name. Silence is not confirmation; the bundle either shrank or it didn't.

Written by Appesto Engineering.