PatternsCore Optimizations
Tree-Shaking Friendly Code
Write code that bundlers can optimize by removing unused exports and dead code.
Tree-Shaking Friendly Code
Problem
Bundlers can't remove unused code if it has side effects or is written in ways that prevent static analysis. This adds unnecessary kilobytes to production bundles.
Solution
Write tree-shakable code using ES modules, pure functions, and avoiding side effects in module scope.
/**
* ✅ Tree-shakable: Named exports
*/
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
// Import only what you need
import { add } from './math'; // Only 'add' is bundled
/**
* ❌ Not tree-shakable: Default export with object
*/
export default {
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
multiply: (a: number, b: number) => a - b,
};
// Imports entire object
import math from './math'; // All functions bundled
/**
* ✅ Tree-shakable: Pure functions without side effects
*/
export function formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
}
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
/**
* ❌ Not tree-shakable: Module with side effects
*/
// This runs immediately when module is imported
console.log('Module loaded');
// Global state mutation
window.appConfig = { version: '1.0.0' };
/**
* ✅ Tree-shakable: Lazy initialization
*/
let cachedConfig: { version: string } | null = null;
export function getConfig(): { version: string } {
if (!cachedConfig) {
cachedConfig = { version: '1.0.0' };
}
return cachedConfig;
}
/**
* Mark pure functions for bundlers
*/
/*#__PURE__*/
function createExpensiveObject(): { data: number[] } {
return { data: Array.from({ length: 1000 }, (_, i) => i) };
}
export const expensiveData = /*#__PURE__*/ createExpensiveObject();Package.json Configuration
{
"name": "my-library",
"version": "1.0.0",
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
},
"./utils": {
"import": "./dist/utils.esm.js",
"require": "./dist/utils.js"
}
},
"sideEffects": false
}Side Effects Configuration
// No side effects (best for tree-shaking)
{
"sideEffects": false
}
// Specific files have side effects
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.ts"
]
}Real-World Examples
Utility Library
// ✅ utils.ts - Tree-shakable utilities
export function debounce<T extends unknown[]>(
fn: (...args: T) => void,
delay: number
): (...args: T) => void {
let timeoutId: ReturnType<typeof setTimeout>;
return (...args: T) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
export function throttle<T extends unknown[]>(
fn: (...args: T) => void,
limit: number
): (...args: T) => void {
let inThrottle = false;
return (...args: T) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
export function memoize<T extends unknown[], R>(
fn: (...args: T) => R
): (...args: T) => R {
const cache = new Map<string, R>();
return (...args: T) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// Import only what you need
import { debounce } from './utils'; // Only debounce is bundledConstants Module
// ✅ constants.ts - Tree-shakable constants
export const API_URL = 'https://api.example.com';
export const TIMEOUT = 5000;
export const MAX_RETRIES = 3;
export const HTTP_STATUS = {
OK: 200,
NOT_FOUND: 404,
SERVER_ERROR: 500,
} as const;
export const ROUTES = {
HOME: '/',
PROFILE: '/profile',
SETTINGS: '/settings',
} as const;
// Import only what you need
import { API_URL, TIMEOUT } from './constants';Class-based Module
// ✅ services.ts - Tree-shakable classes
export class APIClient {
constructor(private baseURL: string) {}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseURL}${endpoint}`);
return response.json() as Promise<T>;
}
}
export class StorageManager {
get<T>(key: string): T | null {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : null;
}
set<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
}
}
// Import only what you need
import { APIClient } from './services';Webpack Configuration
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true, // Mark unused exports
sideEffects: true, // Respect package.json sideEffects
minimize: true, // Remove dead code
},
};Testing Tree-Shaking
# Build and check bundle
npm run build
# Check what's in the bundle
npx webpack-bundle-analyzer dist/stats.json
# Verify tree-shaking worked
grep -r "unusedFunction" dist/
# Should return nothing if tree-shaking workedCommon Pitfalls
❌ Class with Static Initialization
// Not tree-shakable
class Logger {
static instance = new Logger();
log(message: string): void {
console.log(message);
}
}
// Runs on import
export const logger = Logger.instance;✅ Factory Function
// Tree-shakable
export function createLogger(): { log: (message: string) => void } {
return {
log(message: string): void {
console.log(message);
},
};
}
// Only creates when called
const logger = createLogger();Performance Impact
Before Tree-Shaking
// Imports entire lodash (72 KB)
import _ from 'lodash';
_.debounce(fn, 300);
// Bundle size: 72 KB from lodash aloneAfter Tree-Shaking
// Imports only debounce (2 KB)
import debounce from 'lodash-es/debounce';
debounce(fn, 300);
// Bundle size: 2 KB (97% savings)Best Practices
- Use ES modules: Not CommonJS (
require) - Named exports: Not default exports with objects
- Pure functions: No side effects in module scope
- Mark sideEffects: In package.json
- Avoid class static init: Use factory functions
- PURE comments: For complex expressions
- Test regularly: Verify unused code is removed
Checklist for Tree-Shakable Libraries
- Use ES modules (
import/export) - Named exports for each function
- No side effects in module scope
-
sideEffects: falsein package.json - Provide ESM build (
modulefield) - Pure functions only
- Document imports in README
Impact at Scale
For a utility library with 50 functions:
- Without tree-shaking: 150 KB (all functions)
- With tree-shaking: 10 KB (only used functions)
- Savings: 93%
Tree-shaking is essential for modern web applications. It's the difference between a 500 KB bundle and a 50 KB bundle.
Verification
// Check if tree-shaking works
import { add } from './math';
// Build and search dist for unused functions
// If 'multiply' appears in dist, tree-shaking failedWrite tree-shakable code from day one. It's easier than refactoring later.