Skip to content
Now accepting new projects — limited slots available. Get started →
Performance · Updated Aug 4, 2026

What is Tree Shaking?

Tree shaking is a build-time optimization that removes unused JavaScript exports from final bundles.

What is Tree Shaking?

Tree shaking is dead-code elimination for JavaScript exports. The bundler figures out which exports you're actually using and tosses the rest. Rich Harris coined the term when he shipped Rollup in 2015—shake the tree, dead leaves fall off.

It only works with ES modules (import/export). ESM imports are static and declarative, so bundlers can analyze them at build time. CommonJS is dynamic—require() can pull in anything at runtime, so bundlers include everything to be safe.

Webpack added tree shaking in version 2 (2017). Now it's standard in Vite, esbuild, Rollup, every modern toolchain.

In practice, you'll see 20–60% bundle reductions depending on how much of a library you actually use. Classic example: importing just debounce from lodash-es instead of all of lodash drops the contribution from ~70 KB minified to under 1 KB.

How it works

Tree shaking runs during static analysis of your module graph:

  1. Parse — Bundler reads every file, builds an AST.
  2. Mark — Starting from entry points, it follows import statements and marks each referenced export as "used."
  3. Sweep — Any export not marked gets excluded from the output.

This only works with ES modules. CommonJS is runtime-dynamic, so the bundler can't prove what's used. It ships everything.

Code example

// math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }

// app.js
import { add } from './math.js';
console.log(add(2, 3));

After tree shaking, multiply and divide are gone. Stripped entirely.

The sideEffects flag

Bundlers need to know if a module has side effects—polyfills that mutate globals, CSS imports, that kind of thing. If it does, the bundler can't safely remove it even when no exports are consumed.

Library authors declare this in package.json:

{
  "sideEffects": false
}

Or list specific files with side effects:

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

Without this flag, webpack assumes every module might have side effects. Tree shaking becomes way less effective. This is the most common reason tree shaking "doesn't work"—it's the first thing I check.

When to use it

Tree shaking isn't optional in 2026. Every modern bundler does it by default. Real question: is your codebase tree-shakeable?

Do this:

  • Use ES module syntax (import/export) everywhere. No require().
  • Prefer libraries that ship ESM builds—lodash-es over lodash, date-fns over moment.
  • Set "sideEffects": false in your own library's package.json if applicable.
  • Use named exports instead of default exports. Easier for bundlers to trace.
  • Audit with webpack-bundle-analyzer or rollup-plugin-visualizer to verify unused code is actually being dropped.

Skip worrying about it when:

  • You're writing a Node.js server app that doesn't ship code to browsers.
  • Your entire module is consumed—nothing to remove.
  • You're using a framework like Next.js or Astro that already configures this (though you should still verify library choices).

Tree Shaking vs alternatives

Technique Scope When it runs Requires ESM?
Tree shaking Removes unused exports across modules Build time Yes
Dead code elimination Removes unreachable code within a module (e.g., if (false) branches) Build time (minifier) No
Code splitting Splits the bundle into chunks loaded on demand Build time No
Lazy loading Defers loading of chunks until needed at runtime Runtime No

Tree shaking and dead code elimination are complementary. Terser/esbuild handles DCE at the statement level. Tree shaking handles it at the export level.

We've shipped projects where fixing sideEffects alone dropped the JS payload by 40%. Further code splitting cut the initial load by another 30%. They're different tools solving different parts of the same problem.

Real-world example

On a Next.js 14 e-commerce project, the client bundle was 420 KB gzipped. Running @next/bundle-analyzer showed a charting library (recharts) contributing 110 KB even though we only used two chart types.

Root cause: the app imported from the barrel index.js file, and the library's package.json didn't declare sideEffects: false at the time. We switched to direct path imports (recharts/es6/chart/LineChart) and replaced one utility library with a tree-shakeable alternative.

Final bundle: 265 KB gzipped. 37% reduction. Lighthouse Performance score went from 68 to 84 on mobile. The entire fix took about two hours.

Frequently asked questions about Tree Shaking

Is tree shaking the same as dead code elimination?
They're related but different. Dead code elimination (DCE) removes unreachable code within a single module — things like code after a `return` statement or inside an `if (false)` block. Minifiers like Terser and esbuild handle DCE. Tree shaking specifically removes unused *exports* across module boundaries. It answers the question: "Does any other file in this project actually import this function?" In practice, tree shaking runs first to remove unused exports, then DCE cleans up whatever's left inside the remaining code. You want both — and modern bundlers give you both by default.
When did tree shaking become standard?
Rich Harris coined the term and shipped it in Rollup in 2015. Webpack added tree shaking in version 2, released in January 2017, though it required the `sideEffects` flag in `package.json` (introduced in webpack 4, February 2018) to work well in practice. By 2020, with Vite 1.0 and esbuild gaining traction, tree shaking was effectively a default in all major frontend toolchains. As of April 2026, there's no mainstream bundler that doesn't support it. The real milestone was the ecosystem shift to publishing ESM-first packages — that took until roughly 2022–2023 for most popular npm libraries.
What's the alternative to tree shaking?
If you can't tree shake (e.g., stuck with CommonJS dependencies), your main options are: direct path imports to avoid barrel files (`import debounce from 'lodash/debounce'` instead of `import { debounce } from 'lodash'`), manually replacing large libraries with smaller focused packages, or using `babel-plugin-transform-imports` / `babel-plugin-import` to rewrite barrel imports to direct paths automatically. Code splitting via dynamic `import()` helps too — it won't reduce total code, but it defers what's not needed immediately. Long-term though, the best move is to choose libraries that ship proper ESM builds.
Why isn't tree shaking removing unused code from my bundle?
The most common culprits we see: (1) The library uses CommonJS instead of ES modules — tree shaking can't analyze `require()` calls statically. (2) The library or your own `package.json` is missing `"sideEffects": false`, so the bundler conservatively keeps everything. (3) You're importing from a barrel file that re-exports everything, and the barrel itself has side effects. (4) The code you think is unused actually *is* referenced somewhere, possibly through a dynamic reference. Run your bundle through `webpack-bundle-analyzer` or `source-map-explorer` to see exactly what's included and trace back to the import that pulls it in.
Get in touch

Let's build
something together.

Whether it's a migration, a new build, or an SEO challenge — the Social Animal team would love to hear from you.

Get in touch →