JavaScript Tree Shaking: A Complete Tutorial
A complete tutorial on JavaScript tree shaking. Covers how bundlers eliminate dead code, ES module static analysis, side effect declarations, tree-shakable library design, common pitfalls, barrel file issues, and configuring webpack and Rollup for optimal tree shaking.
Tree shaking eliminates unused code from your JavaScript bundles by analyzing ES module imports and removing exports that no consumer references. This tutorial covers how tree shaking works, how to write tree-shakable code, and how to configure bundlers for maximum dead code removal.
For removing dead code patterns, see Removing Dead Code with JS Tree Shaking Guide.
How Tree Shaking Works
// math.js - ES module with named exports
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// app.js - only imports add
import { add } from "./math.js";
console.log(add(2, 3));
// After tree shaking, the bundle contains ONLY:
// function add(a, b) { return a + b; }
// console.log(add(2, 3));
// subtract, multiply, divide are eliminated| Concept | Description |
|---|---|
| Static analysis | Bundler reads import/export at build time, not runtime |
| Used exports | Exports referenced by at least one import statement |
| Dead exports | Exports never imported anywhere in the dependency graph |
| Side effects | Code that runs on import (modifies globals, DOM, etc.) |
| Pure annotations | Hints telling the bundler a call has no side effects |
ES Modules vs CommonJS
// ES Modules: TREE-SHAKABLE (static structure)
// Imports/exports are determined at parse time
import { specific } from "./lib"; // Bundler knows exactly what is used
// CommonJS: NOT TREE-SHAKABLE (dynamic structure)
// Imports/exports are determined at runtime
const lib = require("./lib"); // Bundler cannot know which props are used
const specific = lib.specific; // This pattern is opaque to static analysis
// Dynamic import patterns that BREAK tree shaking:
const name = condition ? "add" : "subtract";
import("./math").then((mod) => mod[name]); // Dynamic key, cannot tree shake
// Destructured require: PARTIALLY works with some bundlers
const { specific } = require("./lib"); // Webpack can sometimes handle this
// ==========================================
// MODULE AUTHORING FOR TREE SHAKING
// ==========================================
// BAD: default export with object (not tree-shakable)
export default {
add(a, b) { return a + b; },
subtract(a, b) { return a - b; },
};
// Consumer: import math from "./math"; math.add(1,2);
// Bundler includes the entire object
// GOOD: named exports (fully tree-shakable)
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// Consumer: import { add } from "./math";
// Bundler includes only addSide Effects Configuration
// package.json - declare your package as side-effect free
{
"name": "my-library",
"version": "1.0.0",
"sideEffects": false
}
// Specific files with side effects
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js",
"./src/global-setup.js"
]
}// What counts as a side effect?
// SIDE EFFECT: modifies global state on import
window.myLib = {};
document.title = "Modified";
// SIDE EFFECT: adds to prototype
Array.prototype.customMethod = function() {};
// SIDE EFFECT: executes code with observable effects
console.log("Module loaded");
fetch("/api/init");
// NOT a side effect: only declares/exports
export function helper() { return 42; }
export const CONSTANT = "value";
export class Widget {}
// PURE annotation: tell bundler this call is safe to remove
const instance = /* #__PURE__ */ createInstance();
export { instance };
// Without #__PURE__, bundler assumes createInstance() has side effects
// and keeps it even if instance is never importedTree-Shakable Library Design
// BAD PATTERN: barrel file re-exporting everything
// index.js
export { Chart } from "./Chart";
export { Table } from "./Table";
export { Form } from "./Form";
export { Modal } from "./Modal";
export { Tooltip } from "./Tooltip";
// Problem: some bundlers struggle with barrel re-exports
// importing { Chart } might pull in Table, Form, Modal, Tooltip too
// GOOD PATTERN: direct deep imports
// Allow consumers to import directly
// import { Chart } from "my-lib/Chart";
// GOOD PATTERN: package.json "exports" field
{
"name": "my-lib",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./Chart": {
"import": "./dist/esm/Chart.js",
"require": "./dist/cjs/Chart.js"
},
"./Table": {
"import": "./dist/esm/Table.js",
"require": "./dist/cjs/Table.js"
}
},
"sideEffects": false
}// TREE-SHAKABLE CLASS PATTERN
// BAD: class with many methods (all or nothing)
export class Utils {
static formatDate(d) { /* ... */ }
static formatCurrency(n) { /* ... */ }
static slugify(s) { /* ... */ }
static truncate(s, len) { /* ... */ }
}
// import { Utils } from "./utils";
// Utils.formatDate(new Date());
// All methods included even if only formatDate is used
// GOOD: individual function exports
export function formatDate(d) {
return d.toISOString().split("T")[0];
}
export function formatCurrency(n) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(n);
}
export function slugify(s) {
return s.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
}
export function truncate(s, len) {
return s.length > len ? s.slice(0, len) + "..." : s;
}
// import { formatDate } from "./utils";
// Only formatDate is bundledWebpack Tree Shaking Configuration
// webpack.config.js
module.exports = {
mode: "production", // Required for tree shaking
optimization: {
usedExports: true, // Mark unused exports
minimize: true, // Remove marked dead code
concatenateModules: true, // Scope hoisting for better shaking
sideEffects: true, // Respect package.json sideEffects
innerGraph: true, // Track usage within modules
splitChunks: {
chunks: "all",
},
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
modules: false, // CRITICAL: do not convert ES modules to CJS
}],
],
},
},
},
],
},
};
// Rollup config (tree shaking is on by default)
// rollup.config.js
export default {
input: "src/index.js",
output: {
file: "dist/bundle.js",
format: "esm",
},
treeshake: {
moduleSideEffects: false, // Assume no module has side effects
propertyReadSideEffects: false, // Property access has no side effects
annotations: true, // Respect #__PURE__ annotations
},
};Analyzing Tree Shaking Results
// Check what was tree-shaken with webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "bundle-report.html",
openAnalyzer: false,
}),
],
};
// Custom script to check tree-shakability
function analyzeExports(sourceCode) {
const exportPattern = /export\s+(function|const|let|var|class)\s+(\w+)/g;
const exports = [];
let match;
while ((match = exportPattern.exec(sourceCode)) !== null) {
exports.push({ type: match[1], name: match[2] });
}
const defaultExport = /export\s+default/.test(sourceCode);
return {
namedExports: exports,
hasDefaultExport: defaultExport,
treeShakable: exports.length > 0 && !defaultExport,
recommendation: defaultExport
? "Convert default export to named exports for better tree shaking"
: "Module uses named exports (tree-shakable)",
};
}| Bundler | Tree Shaking Support | Configuration Required |
|---|---|---|
| Webpack 5 | Yes (production mode) | usedExports: true, sideEffects: true |
| Rollup | Yes (default) | Minimal, enabled by default |
| Vite (Rollup) | Yes (default) | Inherits Rollup defaults |
| esbuild | Yes (default) | --tree-shaking=true (default in bundle mode) |
| Parcel 2 | Yes (default) | Automatic, no config needed |
Rune AI
Key Insights
- Named exports are essential for tree shaking: Default exports with object literals prevent bundlers from identifying which properties are used
- sideEffects: false unlocks full tree shaking: Without this package.json flag, bundlers conservatively keep all modules imported anywhere in the dependency graph
- Babel must preserve ES modules: Configure
modules: falsein @babel/preset-env to prevent converting ES module syntax to CommonJS before the bundler can analyze it - Barrel files can defeat tree shaking: Direct deep imports (
my-lib/Chart) are more reliably tree-shaken than barrel re-exports (my-libthen{ Chart }) - Use #PURE annotations for function calls: Mark factory function calls as pure so bundlers can safely remove them when the result is unused
Frequently Asked Questions
Why does tree shaking not work with CommonJS require()?
What is the role of sideEffects in package.json?
Why do barrel files (index.js re-exports) sometimes break tree shaking?
How do I know if my library is tree-shakable?
Does TypeScript affect tree shaking?
Conclusion
Tree shaking relies on ES modules' static structure to identify and remove dead code at build time. Writing tree-shakable code means using named exports, declaring sideEffects: false, and avoiding patterns that obscure the dependency graph. For advanced dead code removal techniques, see Removing Dead Code with JS Tree Shaking Guide. For bundler architecture, see JavaScript Bundlers: An Advanced Architecture.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.