Bundle Optimization and Tree Shaking in JavaScript
Bundle optimization is critical for performance. This article covers techniques for reducing bundle size through tree shaking, dead code elimination, and compression.
Introduction
Large bundles cause:
- Slow initial load times
- High bandwidth usage
- Poor performance on slow networks
- Wasted resources on unused code
Bundle optimization helps you:
- Reduce bundle size by 30-70%
- Improve Time to Interactive (TTI)
- Reduce bandwidth costs
- Improve user experience
Tree Shaking
What is Tree Shaking?
// math.js - Export multiple functions
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 - Only import what you need
import { add, multiply } from './math.js';
console.log(add(5, 3));
console.log(multiply(5, 3));
// Tree shaking removes subtract and divide from bundle
Enabling Tree Shaking
// webpack.config.js
module.exports = {
mode: 'production', // Enables tree shaking
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true, // Mark unused exports
sideEffects: false // Assume no side effects
}
};
// package.json
{
"name": "my-package",
"version": "1.0.0",
"sideEffects": false, // Enable tree shaking for this package
"main": "index.js",
"module": "index.esm.js" // ES modules for tree shaking
}
Side Effects
// โ Bad: Side effects prevent tree shaking
// utils.js
console.log('Utils loaded'); // Side effect
export function helper() {
return 'help';
}
// app.js
import { helper } from './utils.js'; // Entire module loaded due to side effect
// โ
Good: No side effects
// utils.js
export function helper() {
return 'help';
}
// app.js
import { helper } from './utils.js'; // Only helper is included
// Mark files with side effects
// webpack.config.js
module.exports = {
optimization: {
sideEffects: [
'*.css', // CSS files have side effects
'./src/polyfills.js' // Polyfills have side effects
]
}
};
Dead Code Elimination
Identifying Dead Code
// โ Bad: Unused code
function unusedFunction() {
return 'never called';
}
function usedFunction() {
return 'called';
}
console.log(usedFunction());
// unusedFunction is dead code
// โ
Good: Remove unused code
function usedFunction() {
return 'called';
}
console.log(usedFunction());
Conditional Code
// โ Bad: Conditional code not optimized
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment) {
console.log('Debug info');
// Debug code included in production
}
// โ
Good: Use DefinePlugin for dead code elimination
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
};
// Now dead code is eliminated:
if (false) { // Eliminated by minifier
console.log('Debug info');
}
Minification
JavaScript Minification
// Original code
function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price;
}
return total;
}
const result = calculateTotal([
{ price: 10 },
{ price: 20 }
]);
console.log(result);
// Minified code
function calculateTotal(e){let t=0;for(let l=0;l<e.length;l++)t+=e[l].price;return t}const result=calculateTotal([{price:10},{price:20}]);console.log(result);
// Size reduction: ~60%
Webpack Minification
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production', // Enables minification by default
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true // Remove console statements
},
mangle: true // Shorten variable names
}
})
]
}
};
CSS Optimization
CSS Minification
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
]
};
Unused CSS Removal
// webpack.config.js
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const glob = require('glob');
module.exports = {
plugins: [
new PurgeCSSPlugin({
paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, { nodir: true }),
safelist: ['html', 'body'] // Keep these selectors
})
]
};
Compression
Gzip Compression
// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192, // Only compress files > 8KB
minRatio: 0.8 // Only compress if reduces size by 20%
})
]
};
Brotli Compression
// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');
const zlib = require('zlib');
module.exports = {
plugins: [
new CompressionPlugin({
algorithm: 'brotli',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11 // Maximum compression
}
})
]
};
Bundle Analysis
Webpack Bundle Analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
]
};
// Run: webpack --mode production
// View: bundle-report.html
Analyzing Bundle Composition
// Check what's in your bundle
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'json',
reportFilename: 'bundle-stats.json'
})
]
};
// Parse bundle-stats.json to find large modules
Practical Optimization Strategies
1. Externalize Dependencies
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
// HTML:
// <script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
2. Dynamic Imports
// โ
Good: Load heavy libraries on-demand
async function loadChart() {
const Chart = (await import('chart.js')).default;
return Chart;
}
// Chart.js only loaded when needed
3. Polyfill Optimization
// โ Bad: Include all polyfills
import '@babel/polyfill';
// โ
Good: Include only needed polyfills
import 'core-js/stable';
import 'regenerator-runtime/runtime';
4. Library Alternatives
// โ Bad: Large library
import moment from 'moment'; // 67KB
// โ
Good: Smaller alternative
import { format } from 'date-fns'; // 13KB
Monitoring Bundle Size
CI/CD Integration
// scripts/check-bundle-size.js
const fs = require('fs');
const path = require('path');
function checkBundleSize() {
const bundlePath = path.join(__dirname, '../dist/main.js');
const stats = fs.statSync(bundlePath);
const sizeInKB = stats.size / 1024;
const maxSize = 250; // KB
const maxGzipSize = 80; // KB
console.log(`Bundle size: ${sizeInKB.toFixed(2)}KB`);
if (sizeInKB > maxSize) {
console.error(`โ Bundle exceeds limit: ${sizeInKB.toFixed(2)}KB > ${maxSize}KB`);
process.exit(1);
}
console.log(`โ
Bundle size OK`);
}
checkBundleSize();
// package.json
{
"scripts": {
"build": "webpack --mode production",
"check-size": "npm run build && node scripts/check-bundle-size.js"
}
}
Tracking Over Time
// scripts/track-bundle-size.js
const fs = require('fs');
const path = require('path');
function trackBundleSize() {
const bundlePath = path.join(__dirname, '../dist/main.js');
const stats = fs.statSync(bundlePath);
const sizeInKB = stats.size / 1024;
const historyFile = path.join(__dirname, '../bundle-history.json');
let history = [];
if (fs.existsSync(historyFile)) {
history = JSON.parse(fs.readFileSync(historyFile, 'utf8'));
}
history.push({
date: new Date().toISOString(),
size: sizeInKB,
commit: process.env.GIT_COMMIT || 'unknown'
});
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
console.log(`Bundle size tracked: ${sizeInKB.toFixed(2)}KB`);
}
trackBundleSize();
Best Practices
-
Enable tree shaking:
// โ Good module.exports = { mode: 'production', optimization: { usedExports: true } }; -
Use ES modules:
// โ Good - enables tree shaking export function helper() {} // โ Bad - prevents tree shaking module.exports = { helper }; -
Avoid side effects:
// โ Good - no side effects export function pure() {} // โ Bad - side effect console.log('loaded'); export function impure() {} -
Monitor bundle size:
// โ Good - track in CI/CD npm run check-size -
Use dynamic imports:
// โ Good - load on-demand const module = await import('./heavy.js');
Common Mistakes
-
Not enabling tree shaking:
// โ Bad module.exports = { mode: 'development' }; // โ Good module.exports = { mode: 'production' }; -
Using CommonJS instead of ES modules:
// โ Bad - prevents tree shaking module.exports = { helper }; // โ Good - enables tree shaking export { helper }; -
Not analyzing bundle:
// โ Bad - don't know what's in bundle // โ Good - use BundleAnalyzerPlugin -
Including unnecessary dependencies:
// โ Bad - large library import moment from 'moment'; // โ Good - smaller alternative import { format } from 'date-fns';
Summary
Bundle optimization is essential for performance. Key takeaways:
- Enable tree shaking to remove unused code
- Use ES modules for better optimization
- Minify JavaScript and CSS
- Compress with gzip or brotli
- Analyze bundle composition
- Monitor bundle size in CI/CD
- Use dynamic imports for large libraries
- Choose smaller library alternatives
Related Resources
- Tree Shaking - Webpack
- Bundle Analysis - Webpack
- Minification - Webpack
- Code Splitting - Webpack
- Bundle Size - Web.dev
Next Steps
- Learn about Code Splitting and Lazy Loading
- Explore Caching Strategies: Client, Server, CDN
- Study Web Vitals and Performance Metrics
- Implement tree shaking in your projects
- Monitor bundle size in CI/CD
Comments