Tree Shaking
Eliminate unused code from your production bundles
Tree shaking removes unused code from your JavaScript bundles during the build process. This can significantly reduce bundle size and improve load times.
How Tree Shaking Works
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.ts - Only uses add
import { add } from './math';
console.log(add(1, 2));
// After tree shaking: only add() is in bundle
// subtract, multiply, divide are removedEnabling Effective Tree Shaking
Use ES Modules
// ✅ Good: ES modules are tree-shakeable
import { debounce } from 'lodash-es';
// ❌ Bad: CommonJS imports entire library
const _ = require('lodash');
const { debounce } = require('lodash');Named Imports vs Default
// ✅ Good: Named imports tree-shake well
import { Button, Input } from '@/components/ui';
// ⚠️ Careful: Barrel files can prevent tree shaking
// If components/ui/index.ts re-exports everything
import { Button } from '@/components/ui'; // May load all componentsConfigure sideEffects
{
"sideEffects": false
}
// Or specify files with side effects
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}Common Tree Shaking Issues
Barrel Files
export * from './Button';
export * from './Input';
export * from './Select';
export * from './Modal';
// ... 50 more components
// ❌ Problem: Importing one may bundle all
import { Button } from '@/components';
// ✅ Solution: Direct imports
import { Button } from '@/components/Button';Dynamic Property Access
// ❌ Bad: Dynamic access prevents tree shaking
const icons = {
home: HomeIcon,
settings: SettingsIcon,
user: UserIcon,
};
function Icon({ name }) {
const IconComponent = icons[name]; // All icons bundled
return <IconComponent />;
}
// ✅ Good: Static imports
import { HomeIcon } from './icons/HomeIcon';
function Icon({ name }) {
switch (name) {
case 'home': return <HomeIcon />;
case 'settings': return <SettingsIcon />;
// Only imported icons are bundled
}
}Class Instantiation
// ❌ Bad: Classes are harder to tree shake
import { Validator } from './validators';
const v = new Validator();
// ✅ Good: Pure functions tree shake better
import { validateEmail } from './validators';
validateEmail(email);Next.js Optimization
modularizeImports
module.exports = {
modularizeImports: {
'lodash': {
transform: 'lodash/{{member}}',
},
'@mui/material': {
transform: '@mui/material/{{member}}',
},
'@mui/icons-material': {
transform: '@mui/icons-material/{{member}}',
},
},
};Before:
import { debounce, throttle } from 'lodash';
// Bundles entire lodashAfter (with modularizeImports):
import { debounce, throttle } from 'lodash';
// Transforms to:
// import debounce from 'lodash/debounce';
// import throttle from 'lodash/throttle';optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: [
'lucide-react',
'@headlessui/react',
'date-fns',
'rxjs',
],
},
};Library-Specific Tips
Lodash
// ❌ Bad: 70KB
import _ from 'lodash';
// ❌ Still bad: 70KB (CommonJS)
import { debounce } from 'lodash';
// ✅ Good: ~2KB
import debounce from 'lodash/debounce';
// ✅ Best: ~2KB (ES modules)
import { debounce } from 'lodash-es';date-fns
// ✅ Already tree-shakeable
import { format, parseISO } from 'date-fns';
// Only these functions are bundledMaterial UI
// ❌ Bad: Imports all of MUI
import { Button, TextField } from '@mui/material';
// ✅ Good: Direct imports
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
// ✅ With modularizeImports, named imports work
import { Button, TextField } from '@mui/material';Icon Libraries
// ❌ Bad: All 1000+ icons
import { FaHome } from 'react-icons/fa';
// ✅ Good: Just one icon
import { FaHome } from 'react-icons/fa';
// This is actually fine - react-icons is already optimized
// For lucide-react, use optimizePackageImports
import { Home, Settings } from 'lucide-react';Analyzing Tree Shaking
Bundle Analyzer
npm install @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
# Run analysis
ANALYZE=true npm run buildImport Cost Extension
VS Code extension that shows import sizes inline:
import { debounce } from 'lodash'; // 70KB ❌
import debounce from 'lodash/debounce'; // 2KB ✅Writing Tree-Shakeable Code
Export Pure Functions
// ✅ Tree-shakeable
export function formatDate(date) {
return date.toISOString();
}
export function parseDate(str) {
return new Date(str);
}Avoid Side Effects at Module Level
// ❌ Bad: Side effect prevents tree shaking
console.log('Module loaded');
export function helper() {}
// ✅ Good: No side effects
export function helper() {}Use Explicit Re-exports
// ❌ Bad: Wildcard re-export
export * from './utils';
// ✅ Good: Explicit re-exports
export { formatDate, parseDate } from './utils';Checklist
Project Setup
Use ES modules (import/export) CommonJS cannot be tree-shaken.
Add sideEffects to package.json Tells bundler which files have side effects.
Configure modularizeImports Automatically transform imports to subpaths.
Enable optimizePackageImports Next.js experimental flag for specific packages.
Import Patterns
Use named imports Named exports enable better dead code elimination.
Avoid barrel file wildcards
export * can bundle everything.
Direct import from subpaths Import from 'lodash/debounce' not 'lodash'.
Avoid dynamic property access Static access enables tree shaking.
Library Selection
Prefer ESM versions (lodash-es) ESM libraries are fully tree-shakeable.
Check library tree-shake support Some libraries don't support tree shaking.
Use bundle analyzer Verify what's actually in your bundle.
Consider smaller alternatives date-fns instead of moment, etc.
Impact
| Issue | Bundle Increase | Fix |
|---|---|---|
| Full lodash import | +70KB | Direct import |
| MUI barrel import | +200KB+ | modularizeImports |
| All icons imported | +50-500KB | Direct import |
| moment.js | +300KB | Use date-fns |