Tree Shaking: Eliminate Dead Code from Your Bundle
Tree shaking eliminates unused code automatically. Learn how to write tree-shakeable code and maximize bundle size reduction.
Quick Navigation: How Tree Shaking Works • Writing Tree-Shakeable Code • Library-Specific Patterns • Package Configuration • Verifying Tree Shaking • Best Practices
Quick Decision Guide
Quick Implementation Guide:
Use ES6 modules: Only import/export syntax enables tree shaking. CommonJS (require) doesn't work.
Named imports: import { specific } from 'library' instead of import * as lib from 'library'
Side-effect-free code: Mark packages as side-effect-free in package.json: "sideEffects": false
Direct imports: For lodash, use lodash-es (ES module version) not lodash
Verify results: Use webpack-bundle-analyzer to see what's included in your bundle.
How Tree Shaking Works
Static Analysis
Tree shaking relies on ES6 module syntax which is statically analyzable:
// Static imports (analyzable at build time)
import { functionA, functionB } from './utils';
// Only functionA is used
functionA();
// Result: functionB is removed from bundleBuild Process
1. Bundler analyzes imports: Tracks what's imported and used
2. Marks unused exports: Identifies dead code
3. Removes dead code: Excludes from final bundle
4. Minification: Further optimization
Example
// utils.js
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) { return a / b; }
// app.js
import { add, multiply } from './utils';
console.log(add(2, 3));
// Final bundle only includes:
// - add function
// - multiply is imported but unused (removed)
// - subtract and divide are never imported (removed)Writing Tree-Shakeable Code
Use ES6 Modules
// ✅ Tree-shakeable (ES6)
export function debounce() { /* ... */ }
export function throttle() { /* ... */ }
// ❌ Not tree-shakeable (CommonJS)
module.exports = {
debounce: function() { /* ... */ },
throttle: function() { /* ... */ }
};Named Exports vs Default
// ✅ Better: Named exports
export function getUser() { /* ... */ }
export function deleteUser() { /* ... */ }
import { getUser } from './api';
// Only getUser is bundled
// ⚠️ Less optimal: Default export
export default {
getUser: () => { /* ... */ },
deleteUser: () => { /* ... */ }
};
import api from './api';
api.getUser();
// Entire object is bundled even if deleteUser is unusedAvoid Side Effects
// ❌ Has side effects (can't be tree-shaken)
import './styles.css'; // Modifies global state
console.log('Module loaded'); // Runs on import
export function myFunction() { /* ... */ }
// ✅ No side effects (tree-shakeable)
export function myFunction() { /* ... */ }Library-Specific Patterns
Lodash
// ❌ Bad: Imports entire library (70KB+)
import _ from 'lodash';
_.debounce(fn, 300);
// ⚠️ Better but not optimal
import { debounce } from 'lodash';
// Still imports some shared utilities
// ✅ Best: Use lodash-es
import { debounce } from 'lodash-es';
// Fully tree-shakeable
// ✅ Also good: Direct import
import debounce from 'lodash-es/debounce';Material-UI / MUI
// ❌ Bad: Imports everything
import { Button, TextField, Dialog } from '@mui/material';
// ✅ Good: Direct imports (automatic tree-shaking)
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
// ✅ Also good: Named imports (MUI v5+ tree-shakes automatically)
import { Button, TextField } from '@mui/material';Date Libraries
// ❌ Avoid: Moment.js (not tree-shakeable, 290KB)
import moment from 'moment';
// ✅ Use: date-fns (fully tree-shakeable)
import { format, addDays } from 'date-fns';
// ✅ Or: Day.js (smaller alternative, 2KB)
import dayjs from 'dayjs';Package Configuration
package.json - sideEffects
Tell bundlers which files have side effects:
{
"name": "my-library",
"sideEffects": false
}sideEffects: false = "All files are side-effect-free, tree-shake aggressively"
Specific Side Effects
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}Only these files have side effects, everything else is tree-shakeable.
Module Fields
{
"main": "dist/index.cjs.js", // CommonJS (not tree-shakeable)
"module": "dist/index.esm.js", // ES6 (tree-shakeable)
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
}
}
}Modern bundlers prefer the "module" or "exports.import" field.
Verifying Tree Shaking
Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};Vite Bundle Analysis
npm run build -- --mode analyze// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
})
]
};Check Import Size
// Before optimization
import _ from 'lodash'; // 70KB
// After optimization
import { debounce } from 'lodash-es'; // 5KB
// Savings: 65KB (93% reduction)Real Example
Before tree shaking:
bundle.js: 850KB
- lodash: 70KB
- moment: 290KB
- unused utils: 100KB
After tree shaking:
bundle.js: 390KB (54% reduction)
- lodash (debounce only): 5KB
- date-fns (format only): 2KB
- unused code: removedBest Practices
Do's
Don'ts
Common Mistakes
1. Mixing Module Systems
// ❌ Mixed (breaks tree shaking)
import { func1 } from './module1'; // ES6
const func2 = require('./module2'); // CommonJS
// ✅ Consistent ES6
import { func1 } from './module1';
import { func2 } from './module2';2. Re-exporting Everything
// ❌ Barrel export (imports everything)
// index.js
export * from './module1';
export * from './module2';
// ✅ Named re-exports
export { specific, functions } from './module1';3. Side Effects in Modules
// ❌ Side effect on import
// utils.js
console.log('Utils loaded');
export function add() { /* ... */ }
// ✅ No side effects
export function add() { /* ... */ }