Bundle optimization is crucial for delivering fast web applications. Large bundles lead to slow load times, poor user experience, and lower search rankings. Let's explore advanced techniques for optimizing your bundles.
Tree Shaking
Tree shaking eliminates dead code from your bundles. Modern bundlers use static analysis to remove unused exports.
ES6 Modules for Tree Shaking
javascript
// ❌ CommonJS - tree shaking doesn't work
const utils = require('./utils');
utils.util1(); // util2 and util3 still included
// ✅ ES6 modules - tree shaking works
import { util1 } from './utils';
util1(); // Only util1 included
// utils.js
export const util1 = () => { /* ... */ };
export const util2 = () => { /* ... */ }; // Removed if unused
export const util3 = () => { /* ... */ }; // Removed if unusedSide Effects Configuration
json
// package.json
{
"sideEffects": false, // All modules are side-effect free
"sideEffects": ["*.css", "*.scss"], // Only CSS has side effects
"sideEffects": ["./src/polyfill.js"] // Specific files have side effects
}javascript
// Mark pure functions for better tree shaking
export const pureFunction = /*#__PURE__*/ (() => {
return function() {
return 'pure';
};
})();Code Splitting
Route-Based Splitting
javascript
// React Router with lazy loading
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}Component-Based Splitting
javascript
// Lazy load heavy components
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>
Show Chart
</button>
{showChart && (
<Suspense fallback={<LoadingSpinner />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}Dynamic Imports
javascript
// Load modules on demand
async function processFile(file) {
// Only load file processing library when needed
const { parseFile, extractData } = await import('./lib/file-processor');
return extractData(parseFile(file));
}
// Conditional loading
async function initializeMap() {
if (needsMap) {
const { initMap } = await import('./lib/map-loader');
initMap();
}
}Webpack Optimization
Production Configuration
javascript
// webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.logs
pure_funcs: ['console.log'], // Remove specific calls
},
format: {
comments: false, // Remove comments
},
},
extractComments: false, // Don't extract comments
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
runtimeChunk: {
name: 'runtime',
},
},
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192, // Only compress files larger than 8KB
minRatio: 0.8, // Only compress if compression ratio is better than 0.8
}),
],
};Module Resolution Optimization
javascript
module.exports = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'], // Avoid extensions in imports
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
modules: [path.resolve(__dirname, 'node_modules')], // Faster resolution
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // Cache babel results
cacheCompression: false, // Faster cache
},
},
},
],
},
};Vite Optimization
Build Configuration
javascript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteCompression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
react(),
viteCompression({
algorithm: 'gzip',
ext: '.gz',
}),
viteCompression({
algorithm: 'brotliCompress',
ext: '.br',
}),
],
build: {
target: 'esnext',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-library': ['@mui/material', 'framer-motion'],
},
},
},
chunkSizeWarningLimit: 1000, // 1MB warning
},
esbuild: {
drop: ['console', 'debugger'], // Remove console.logs
},
});Compression Strategies
Brotli Compression
javascript
// Express.js with Brotli compression
import express from 'express';
import compression from 'compression';
import expressStaticGzip from 'express-static-gzip';
const app = express();
// Serve pre-compressed files
app.use(
expressStaticGzip('dist', {
enableBrotli: true,
orderPreference: ['br', 'gz'],
})
);
// Fallback to on-the-fly compression
app.use(compression({
threshold: 1024, // Only compress files larger than 1KB
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
}));Image Optimization
javascript
// Next.js Image Optimization
import Image from 'next/image';
export default function Page() {
return (
<Image
src="/hero-image.jpg"
alt="Hero"
width={1200}
height={600}
priority // Above the fold
placeholder="blur" // Blur placeholder
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
// Modern image formats
// Automatically converts to WebP/AVIFRuntime Optimization
Lazy Loading Images
javascript
// Intersection Observer for lazy loading
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.srcset = img.dataset.srcset;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});Prefetch and Preload
html
<!-- Preload critical resources -->
<link rel="preload" href="/styles/main.css" as="style">
<link rel="preload" href="/scripts/main.js" as="script">
<!-- Prefetch likely next pages -->
<link rel="prefetch" href="/dashboard">
<link rel="prefetch" href="/settings">
<!-- Preconnect to external origins -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">Bundle Analysis
Webpack Bundle Analyzer
javascript
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
}),
],
};Performance Budgets
javascript
// webpack.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
performance: {
maxEntrypointSize: 244000, // 244KB
maxAssetSize: 244000,
hints: 'warning',
},
});Monitoring and Metrics
Bundle Size Tracking
javascript
// Track bundle sizes in CI
const fs = require('fs');
const gzipSize = require('gzip-size');
const files = [
'dist/main.js',
'dist/vendor.js',
'dist/main.css',
];
files.forEach(file => {
const content = fs.readFileSync(file);
const size = content.length;
const gzipped = gzipSize.sync(content);
console.log(`${file}:`);
console.log(` Size: ${(size / 1024).toFixed(2)} KB`);
console.log(` Gzipped: ${(gzipped / 1024).toFixed(2)} KB`);
if (gzipped > 244000) {
console.error(` ⚠️ Exceeds 244KB budget!`);
process.exit(1);
}
});Best Practices
- 1Measure First: Always analyze bundle size before optimizing
- 2Lazy Load Routes: Split by route for optimal initial load
- 3Tree Shake Aggressively: Use ES6 modules and mark side-effect-free code
- 4Compress Everything: Use Brotli and Gzip for text-based assets
- 5Optimize Images: Use modern formats and responsive images
- 6Set Budgets: Enforce bundle size limits in CI/CD
- 7Monitor Continuously: Track bundle sizes over time
- 8Update Dependencies: Newer versions often include optimizations
Conclusion
Bundle optimization is an ongoing process. Start with code splitting and tree shaking, implement compression strategies, and continuously monitor your bundle sizes. The key is to deliver only what's needed, when it's needed, in the most efficient format possible.