Tree shaking isn’t about finding and deleting unused code; it’s about the JavaScript engine’s ability to not include code it knows it won’t execute.

Let’s see it in action. Imagine you have a utility library, my-utils, with functions for math and string manipulation:

// my-utils.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

And in your application, you only use add:

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

const result = add(5, 3);
console.log(result);

When you bundle app.js with a tool like Webpack or Rollup configured for tree shaking, the final output bundle won’t contain subtract or capitalize, even though they are exported from my-utils.js.

This works because modern JavaScript modules (import/export) are static. This means the dependency graph – which module imports from which other module – can be determined at build time without executing any code. Tree shaking tools analyze this static graph. They start from your application’s entry point and traverse all imported modules. If a module or a specific export from a module is never imported by any part of the code that is reached, it’s considered "dead" and is omitted from the final bundle.

The primary problem tree shaking solves is the bloat of JavaScript bundles. As applications grow, they often pull in many libraries. Even if you only use a small fraction of a library’s functionality, a naive bundler might include the entire library, leading to slower download times, increased memory usage, and degraded performance for your users. Tree shaking drastically reduces bundle size by ensuring only the code that’s actually executed is shipped.

Here are the key levers you control:

  • Module Format: You must use ES Modules (import/export). CommonJS (require/module.exports) is dynamic and cannot be reliably tree shaken because require calls can happen conditionally or dynamically, making it impossible for a build tool to know at build time what will be imported.
  • Bundler Configuration: Your bundler (Webpack, Rollup, Parcel, esbuild) needs to be configured to enable tree shaking. For Webpack, this typically involves setting mode: 'production' (which enables optimizations like tree shaking by default) and ensuring your package.json has "sideEffects": false or a more granular configuration to tell Webpack which files do have side effects (e.g., CSS imports, polyfills that modify global objects). Rollup is generally more aggressive with tree shaking out-of-the-box.
  • Library Authoring: If you’re writing a library, you should follow ES Module best practices. Avoid side effects in your module’s top level. If a function must be imported for its side effects (e.g., a polyfill that modifies Array.prototype), you need to explicitly tell the bundler about it using the "sideEffects" field in your package.json.
  • Code Structure: Be mindful of how you import. import * as utils from 'my-utils' will often prevent tree shaking of individual functions within my-utils because the bundler sees that the entire module namespace might be used. Prefer import { add } from 'my-utils' when possible.

The one thing most people miss is that tree shaking isn’t a magic bullet for all unused code. If a function is imported but never called within the reachable code, it’s shaken. However, if a function is exported and imported, but that imported function is never actually invoked in your application’s execution path, it will not be shaken. The bundler only knows what’s imported, not necessarily what’s executed downstream if that execution is conditional on runtime logic. This is why libraries that expose a large API surface, even if you only use a few methods, can still lead to larger bundles if their internal structure isn’t optimized for static analysis.

The next hurdle is understanding how side effects in modules can break tree shaking and how to manage them.

Want structured learning?

Take the full Npm course →