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 } }; ```javascript - Use ES modules:
// ✅ Good - enables tree shaking export function helper() {} // ❌ Bad - prevents tree shaking module.exports = { helper }; ```javascript - Avoid side effects:
// ✅ Good - no side effects export function pure() {} // ❌ Bad - side effect console.log('loaded'); export function impure() {} ```javascript - Monitor bundle size:
// ✅ Good - track in CI/CD npm run check-size ```javascript - Use dynamic imports:
// ✅ Good - load on-demand const module = await import('./heavy.js'); ```javascript
Common Mistakes
- Not enabling tree shaking:
// ❌ Bad module.exports = { mode: 'development' }; // ✅ Good module.exports = { mode: 'production' }; ```javascript - Using CommonJS instead of ES modules:
// ❌ Bad - prevents tree shaking module.exports = { helper }; // ✅ Good - enables tree shaking export { helper }; ```javascript - Not analyzing bundle:
// ❌ Bad - don't know what's in bundle // ✅ Good - use BundleAnalyzerPlugin ```javascript - 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