Webpack’s tree shaking can actually increase your bundle size if you’re not careful about how you structure your code.

Let’s see it in action. Imagine you have a utility library and an application that uses only a small part of it.

First, the library (utils.js):

// utils.js
export function add(a, b) {
  console.log('Adding!');
  return a + b;
}

export function subtract(a, b) {
  console.log('Subtracting!');
  return a - b;
}

export function multiply(a, b) {
  console.log('Multiplying!');
  return a * b;
}

export function divide(a, b) {
  console.log('Dividing!');
  return a / b;
}

Now, the application (app.js) that only uses add:

// app.js
import { add } from './utils';

console.log('Result:', add(5, 3));

To make tree shaking work, we need a few things in our Webpack configuration (webpack.config.js):

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production', // Crucial for tree shaking
  entry: './app.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // No specific tree shaking config needed for ES Modules
};

When we run npx webpack, Webpack analyzes the import statements. Because utils.js uses ES Module export syntax, Webpack can statically analyze it. It sees that app.js only imports add. In the production mode, Webpack performs optimizations, and tree shaking is one of them. It will then exclude subtract, multiply, and divide from the final bundle.js.

The output bundle.js will look something like this (minified and mangled, but conceptually):

// dist/bundle.js (simplified)
function add(a,b){console.log("Adding!");return a+b}console.log("Result:",add(5,3));

Notice subtract, multiply, and divide are gone. This is tree shaking at work: unused code is pruned.

The core problem tree shaking solves is the ever-growing size of JavaScript bundles. As applications become more complex and rely on numerous libraries, including only the code that’s actually executed becomes critical for fast load times. Webpack achieves this by performing static analysis on your code, primarily focusing on ES Module import and export statements. It builds a dependency graph and then identifies and removes any "dead" code – modules or functions that are imported but never referenced in the execution path.

To enable this, you must:

  1. Use ES Modules: import and export syntax is paramount. CommonJS require() statements, especially dynamic ones (require(variable)), are much harder for Webpack to analyze statically and can prevent effective tree shaking.
  2. Set mode to production: This is where Webpack enables its optimization passes, including tree shaking. In development mode, these optimizations are typically disabled for faster build times.
  3. Ensure your libraries are compatible: Libraries that use ES Modules for their exports will be tree-shakeable. If a library only exposes its API via a global variable or uses CommonJS internally without proper ES Module wrappers, its code might not be pruned.

Consider this scenario: you’re using a large charting library, but you only need one specific chart type. If the library is structured with ES Modules, Webpack can potentially shake out all the code for other chart types, significantly reducing your bundle.

A common pitfall is using libraries that export their entire API as a single object. For example, if a library does this:

// Bad for tree shaking library.js
const utils = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
};
module.exports = utils; // Or export default utils;

And you import it like:

// app.js
const utils = require('./library'); // Or import utils from './library';
console.log(utils.add(1, 2));

Webpack sees that utils as a whole is imported. Even if you only use utils.add, it cannot definitively know that subtract (or any other property on utils) is unused without executing the code, which it can’t do during static analysis. Thus, the entire library.js content might be included. The fix is to have the library export individual functions:

// Good for tree shaking library.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

Then in app.js:

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

Now, subtract can be shaken.

Even with ES Modules, side effects in modules can prevent them from being shaken. A module is considered to have side effects if it performs actions beyond just exporting values – for example, modifying a global variable, making an API call, or logging to the console directly at the top level of the module. Webpack tries to detect these, but sometimes you need to explicitly tell it which modules are "side effect free" or which ones do have side effects.

You can configure this in package.json using the "sideEffects" field:

// package.json
{
  "name": "my-app",
  "version": "1.0.0",
  "sideEffects": false, // Tells Webpack that *no* modules in this package have side effects.
  // OR
  "sideEffects": [
    "./src/module-with-side-effects.js", // List specific files with side effects.
    "*.css" // Example: CSS imports often have side effects (injecting styles).
  ],
  // ... other package details
}

Setting "sideEffects": false tells Webpack that it’s safe to remove any module from this package if it’s not imported. If you have modules with side effects, listing them explicitly allows Webpack to be more aggressive with tree shaking on other modules.

The next hurdle is understanding how Babel or other transpilers interact with ES Modules and tree shaking.

Want structured learning?

Take the full Webpack course →