Tree Shaking Reference Guide

Master the art of eliminating dead code from your JavaScript bundles through ES modules and modern bundler optimization techniques.

What Is Tree Shaking?

Tree shaking is a form of dead-code elimination that has become a must-have optimization technique when bundling JavaScript applications. The term was popularized by the Rollup team and has since been adopted by virtually every modern JavaScript bundler.

Imagine your application as a tree. The source code and libraries you actually use represent the green, living leaves of the tree, while unused code represents the brown, dead leaves. By shaking the tree, you cause the dead leaves to fall, leaving only the essential code in your final bundle.

At its core, tree shaking works by analyzing the static import and export relationships in your code to determine which modules and exports are actually used by your application. Any code that cannot be reached through this analysis is considered dead code and can be safely removed from the final bundle without affecting the application's behavior.

Why Tree Shaking Matters

Modern JavaScript applications have grown exponentially in complexity, and with that complexity comes a significant increase in the amount of JavaScript code shipped to browsers. Larger JavaScript payloads directly impact page load times, time to interactive, and overall user experience. A typical application might include hundreds of kilobytes of unused code from dependencies, code that users download but never execute. Tree shaking eliminates this dead weight, ensuring users download only the code your application actually needs. Combined with proper web development practices, this optimization significantly improves site performance and user satisfaction.

What You'll Learn

This guide covers:

  • How tree shaking works with ES modules
  • Configuring webpack and other bundlers
  • Using the sideEffects property effectively
  • Common pitfalls and troubleshooting
  • Best practices for library authors and consumers

ES Modules vs CommonJS

Understanding the difference between ES modules and CommonJS is crucial for understanding how tree shaking works.

CommonJS Limitations

CommonJS was the first widely adopted module system for JavaScript outside of browsers. It introduced the require() function for importing modules and module.exports for exporting functionality. While CommonJS solved the problem of code organization in Node.js applications, it has a fundamental limitation: the require() function is dynamic.

With CommonJS, you can conditionally require modules based on runtime conditions:

if (someCondition) {
 const module = require('./module-a');
} else {
 const module = require('./module-b');
}

Dynamic requires prevent tree shaking because bundlers cannot determine at build time which modules will actually be used. The decision of which module to load depends on runtime conditions that can only be evaluated when the code executes. Without knowing which paths through the code will be taken, the bundler must include all possible modules.

ES Modules: The Static Alternative

ES modules, introduced in ES2015, take a fundamentally different approach. Instead of using functions for imports and exports, ES modules use dedicated keywords: import and export. These statements are only allowed as top-level declarations and cannot be nested inside functions, conditionals, or other dynamic structures.

This static structure means that a bundler can analyze the entire module dependency graph at build time without executing any code:

// math.js
export function square(x) {
 return x * x;
}

export function cube(x) {
 return x * x * x;
}

// index.js
import { cube } from './math.js';
console.log(cube(5));

The static structure enables tree shaking analysis. The bundler traces import chains through your application, identifies which exports are actually used, and eliminates everything else. In this example, the square function is imported but never used, so a tree shaking enabled bundler would remove it from the final bundle, leaving only the cube function.

Key Differences Summary

AspectCommonJSES Modules
Import syntaxrequire() functionimport keyword
Export syntaxmodule.exportsexport keyword
When evaluatedRuntime (dynamic)Build time (static)
Conditional importsSupportedNot supported
Tree shakingNot possibleFully supported
Circular referencesSupportedSupported with caveats

The static nature of ES modules is what makes tree shaking possible. For effective dead code elimination, use ES modules throughout your application and configure your build tools to preserve their structure.

How Tree Shaking Works

Tree shaking works through a multi-stage process that analyzes your code's module structure and removes unused exports.

The Four Stages

  1. Module Graph Construction

When you run your bundler, it builds a complete dependency graph of your application by following all import statements. This graph includes every module in your application and tracks which modules export which functionality and which modules import those exports.

  1. Mark and Sweep

After building the dependency graph, the bundler marks all exports that are actually used by the application. It starts from your entry point and follows the import chains, marking each export that is imported and referenced in the application code.

  1. Elimination

The bundler removes all unreachable code from the final bundle. This includes entire modules that are never imported, individual exports that are imported but never used, and any code that becomes unreachable once the unused exports are removed.

  1. Minification

After elimination, the remaining code goes through minification, which renames variables, removes whitespace and comments, and performs other optimizations that further reduce bundle size. Tools like Terser perform additional dead code elimination at the statement level.

What Tree Shaking Cannot Eliminate

Tree shaking operates at the module export level, not at the statement level within functions. If a function is exported and imported, the entire function remains in the bundle even if parts of it are never executed. Only minification can remove unused code within functions.

export function usedFunction() {
 const unusedVariable = calculateExpensiveValue();
 return "result";
}

export function anotherUsedFunction() {
 return "another result";
}

function calculateExpensiveValue() {
 return 42;
}

In this example, tree shaking could remove calculateExpensiveValue if it's not exported, but it cannot remove the unusedVariable declaration within usedFunction. That's where minification with Terser helps by eliminating unreachable statements.

Understanding these limitations helps you write code that maximizes tree shaking effectiveness. Structure your modules with granular exports so unused functionality can be eliminated at the module level.

Configuring Your Bundler for Tree Shaking

Proper configuration is essential for tree shaking to work effectively.

Webpack Configuration

Webpack 4 and later versions have built-in support for tree shaking and enable it automatically when building in production mode:

// webpack.config.js
module.exports = {
 mode: 'production',
};

Production mode automatically enables tree shaking through the usedExports option and enables minification through the minimize option. For more granular control in development mode:

module.exports = {
 mode: 'development',
 optimization: {
 usedExports: true,
 minimize: true,
 },
};

The usedExports option enables the analysis that determines which exports are actually used. The minimize option enables the minification plugin (typically Terser) that performs the actual code elimination.

Babel Configuration

Babel is a widely used transpiler that converts modern JavaScript into backwards-compatible code. However, Babel's default behavior can break tree shaking if not configured correctly. The problem occurs when Babel converts ES modules to CommonJS modules, replacing import and export statements with require and module.exports.

This destroys the static structure that tree shaking relies on. To prevent this, configure Babel to preserve ES modules:

// babel.config.js
module.exports = {
 presets: [
 ['@babel/preset-env', {
 modules: false,
 }],
 ],
};

Setting modules to false tells Babel to leave ES modules as-is, preserving their static structure for the bundler to analyze. This is critical for effective tree shaking.

Rollup Configuration

Rollup was the creator of the tree shaking concept and has it built-in by default. When you use Rollup, tree shaking works out of the box without any special configuration. Other bundlers like Vite and Parcel also provide first-class tree shaking support following similar principles.

The key across all bundlers is ensuring your build pipeline preserves ES module structure from source to final bundle.

The sideEffects Property

The sideEffects property in package.json is one of the most powerful tools for optimizing tree shaking.

Understanding Side Effects

A side effect occurs when code modifies something outside of its own scope or depends on external state. Functions with side effects are considered impure, while functions without side effects are pure. Pure functions always return the same result for the same inputs, regardless of when or how they're called.

In the context of tree shaking, a side effect at the module level means importing the module causes some behavior beyond just making exports available. Common examples include CSS files that apply styles when imported, polyfills that modify the global scope, and initialization code that runs when a module is imported.

When a module has side effects, the bundler cannot safely remove it even if none of its exports are used, because removing the module would also remove the side effects that the application depends on.

Declaring No Side Effects

{
 "name": "my-package",
 "sideEffects": false
}

Setting sideEffects to false tells the bundler that this package has no side effects and that any unused exports can be safely eliminated. This enables more aggressive tree shaking and can significantly reduce bundle size by allowing the bundler to skip entire modules that aren't used.

Selective Side Effects

When not all files in your package have side effects, you can explicitly declare which files do:

{
 "name": "my-package",
 "sideEffects": [
 "./src/has-side-effects.js",
 "*.css"
 ]
}

This tells the bundler that only the specified files have side effects. All other files can be freely tree shaken if their exports are unused, while CSS files and other files with side effects are preserved.

When to Use sideEffects

Use sideEffects: false when your package has no side effects at all, which is common for utility libraries and pure function packages. Use explicit file lists when your package includes CSS imports, global modifications, initialization code, or re-exports that must be evaluated. When uncertain, explicit is safer than false, as incorrect tree shaking could break applications.

For library authors, declaring sideEffects correctly benefits your users by enabling more aggressive optimization of their bundles.

Common Pitfalls and Troubleshooting

Even with proper configuration, tree shaking can fail to eliminate all unused code.

Imported but Never Used

import { unusedFunction } from './utils';
// unusedFunction is imported but never called

This is the most straightforward case. Modern bundlers automatically remove unused imports, but the key is ensuring your configuration doesn't prevent this optimization. Check that production mode is enabled and no transforms are converting ES modules to CommonJS.

Re-exports with Side Effects

// index.js
export { something } from './has-side-effects';

Re-exports present a challenge because the bundler cannot simply remove the export if something is unused. The import from has-side-effects might trigger side effects that the application depends on, even if something itself isn't used. This is where the sideEffects property becomes critical for library authors.

Nested Dependencies

Tree shaking effectiveness depends on your dependencies also supporting it. If a dependency imports and re-exports from another package, the bundler may not be able to tree shake those re-exports if the intermediate package doesn't properly declare sideEffects. This creates a chain of optimization--your application can only be as tree-shakeable as your least-optimized dependency.

Conditional Imports

While ES modules are static, seemingly conditional code can prevent tree shaking:

import { feature } from './features';

if (feature.enabled) {
 feature.init();
}

If feature.enabled could be true at runtime, the bundler must include the feature module even if it can't determine whether the condition will pass. Write code that allows the bundler to determine usage at build time.

Testing Tree Shaking

Verify tree shaking is working by generating a bundle size report using your bundler's built-in analysis tools. Compare bundle sizes with and without unused imports to confirm the difference. Use source map explorer or webpack-bundle-analyzer to visualize exactly what's included in your bundle.

Regular bundle audits help identify optimization opportunities and catch configuration issues before they affect production performance.

Impact on Bundle Size

The impact of tree shaking on bundle size depends on several factors, including your dependencies and their tree-shaking compatibility. For comprehensive performance optimization, consider pairing tree shaking with professional SEO services that address page speed as a ranking factor.

Bundle Size Reduction Examples

In ideal scenarios, tree shaking can eliminate significant portions of unused code. Libraries with many utility functions that you only use a few of can see substantial reductions. For example, the ES module version of lodash (lodash-es) can often be reduced from hundreds of kilobytes to just the few functions you actually use.

For large applications with many dependencies, effective tree shaking can reduce bundle sizes by 30% or more compared to non-tree-shaken bundles. This translates directly to faster page loads, improved time to interactive, and better user experience across all devices.

Combined with Other Optimizations

Tree shaking works best combined with other optimization techniques:

  • Minification - Terser eliminates unused code at the statement level after tree shaking removes unused exports
  • Compression - gzip and brotli compression further reduce transmitted size of the optimized bundle
  • Code splitting - Divides your bundle into smaller chunks that load on demand, complementing tree shaking

Each optimization stage contributes differently: tree shaking eliminates unused code at the module level, minification optimizes the remaining code at the statement level, and compression reduces the transmitted size of the final output.

When Tree Shaking Has Less Impact

Tree shaking provides less benefit in certain scenarios: small applications with minimal dependencies, applications that already use only what's needed, libraries with few exports or all-used exports, and codebases where most imports are utilized. In these cases, focus optimization efforts on other areas like image optimization, caching strategies, and server-side improvements.

Regular bundle analysis helps you understand where optimization efforts will have the most impact for your specific application.

Best Practices

For Application Developers

  • Use ES modules for all code and dependencies throughout your application
  • Configure bundler for production mode to enable automatic tree shaking
  • Configure Babel to preserve ES modules (modules: false) to prevent CommonJS conversion
  • Choose dependencies that support tree shaking by declaring sideEffects appropriately
  • Use named imports instead of default imports when possible for more granular elimination
  • Audit bundle size regularly with analysis tools to identify optimization opportunities

For Library Authors

  • Use ES modules for all exports to enable tree shaking for your consumers
  • Declare sideEffects: false when your package has no side effects to maximize user benefit
  • Use named exports instead of default exports for more granular consumption
  • Avoid side effects in module scope that prevent safe elimination
  • Consider providing both ES module and CommonJS builds for compatibility
  • Document any intentional side effects so consumers understand what cannot be eliminated

For Both

Avoid exporting objects with mixed used and unused properties--consumers cannot partially use an object export. Be careful with re-exports from third-party packages, as these may prevent tree shaking if the source has side effects. Consider using dynamic imports for code you might not need immediately, enabling lazy loading alongside tree shaking. Test tree shaking effectiveness with bundle analysis tools to verify your optimizations are working. Keep dependencies up to date for the latest tree shaking improvements.

By following these practices, you ensure your code is optimized for tree shaking and your applications perform at their best.

Frequently Asked Questions

Ready to Optimize Your JavaScript Bundles?

Our web development team specializes in building high-performance applications with optimized bundle sizes and fast load times.