š¦
Intermediate
12 min readBundle Size Optimization
Tree shaking, code splitting, and reducing JavaScript bundle size
Understanding Bundle Optimization
Bundle size directly impacts load time and user experience. Smaller bundles mean faster downloads, parsing, and execution. Modern bundlers provide powerful optimization techniques.
Tree Shaking
Tree shaking removes unused code from your bundle. It works with ES6 modules (import/export).
// ā Bad: Import entire library
import _ from 'lodash'; // Imports 70KB+
const result = _.debounce(fn, 300);
// ā
Good: Import only what you need
import debounce from 'lodash/debounce'; // Imports only 2KB
// ā
Better: Use lodash-es for better tree shaking
import { debounce } from 'lodash-es';
// Example with date-fns
// ā Bad
import * as dateFns from 'date-fns';
dateFns.format(new Date(), 'yyyy-MM-dd');
// ā
Good
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');
Code Splitting Strategies
// 1. Route-based splitting
import { lazy, Suspense } from 'react';
// Instead of importing all routes upfront
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
}>
} />
} />
} />
);
}
// 2. Component-based splitting
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
{showChart && (
}>
)}
);
}
// 3. Vendor splitting (webpack config)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
Bundle Analysis
// Install bundle analyzer
// npm install --save-dev 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'
})
]
};
// For Next.js
// npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// your next config
});
// Run: ANALYZE=true npm run build
// For Vite
// npm install --save-dev rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({
open: true,
filename: 'dist/stats.html'
})
]
};
Dead Code Elimination
// Webpack automatically removes dead code in production
// ā This code is never used and will be removed
function unusedFunction() {
console.log('This is never called');
}
// ā
Mark side-effect-free modules in package.json
{
"name": "my-library",
"sideEffects": false
}
// Or specify files with side effects
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}
// Use /*#__PURE__*/ comment for pure functions
const result = /*#__PURE__*/ expensiveFunction();
// Terser will remove if result is unused
Import Optimization
// ā Bad: Barrel imports that import everything
// components/index.js
export { Button } from './Button';
export { Input } from './Input';
export { Select } from './Select';
// ... 50 more components
// app.js
import { Button } from './components'; // Imports ALL components!
// ā
Good: Direct imports
import { Button } from './components/Button';
// ā
Good: Configure babel-plugin-import for auto-optimization
// .babelrc
{
"plugins": [
["import", {
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}]
]
}
// Now this is optimized automatically
import { Button, DatePicker } from 'antd';
Minification and Compression
// Webpack production mode includes:
// - Minification (Terser)
// - Dead code elimination
// - Scope hoisting
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.logs
drop_debugger: true,
pure_funcs: ['console.log'] // Remove specific functions
},
mangle: true, // Shorten variable names
output: {
comments: false // Remove comments
}
},
extractComments: false
})
]
}
};
// Enable gzip/brotli compression
// Server-side (Express)
const compression = require('compression');
app.use(compression());
// Nginx config
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
Dynamic Imports for Heavy Libraries
// Load heavy libraries only when needed
// ā Bad: Always loaded
import moment from 'moment';
const formatted = moment().format('YYYY-MM-DD');
// ā
Good: Load on demand
async function formatDate(date) {
const moment = await import('moment');
return moment.default(date).format('YYYY-MM-DD');
}
// ā
Better: Use lighter alternatives
import { format } from 'date-fns'; // Much smaller than moment
const formatted = format(new Date(), 'yyyy-MM-dd');
// Example: Load PDF generator only when needed
async function generatePDF(data) {
const jsPDF = await import('jspdf');
const doc = new jsPDF.default();
doc.text(data, 10, 10);
doc.save('document.pdf');
}
button.addEventListener('click', () => generatePDF(data));
Optimize Dependencies
// Check dependency sizes before installing
// npm install -g cost-of-modules
// cost-of-modules
// Replace heavy libraries with lighter alternatives:
// ā Heavy: moment (231KB)
import moment from 'moment';
// ā
Light: date-fns (75KB, tree-shakeable)
import { format, addDays } from 'date-fns';
// ā Heavy: lodash (71KB)
import _ from 'lodash';
// ā
Light: lodash-es (tree-shakeable)
import { debounce, throttle } from 'lodash-es';
// ā
Lightest: Native methods or small utilities
const debounce = (fn, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
};
// ā Heavy: axios (13KB)
import axios from 'axios';
// ā
Light: native fetch (0KB)
fetch('/api/data').then(r => r.json());
// Or use: ky (3KB) or redaxios (1KB) for axios-like API
Webpack Module Federation
// Share modules across micro-frontends
// webpack.config.js (Host app)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// Remote app
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
'./Header': './src/Header'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
});
// Usage in host
const RemoteButton = React.lazy(() => import('app1/Button'));
Measuring Bundle Size
# Using bundlesize
npm install --save-dev bundlesize
# package.json
{
"scripts": {
"test:size": "bundlesize"
},
"bundlesize": [
{
"path": "./dist/bundle.js",
"maxSize": "200 kB"
},
{
"path": "./dist/vendor.js",
"maxSize": "150 kB"
}
]
}
# Fails CI if bundle exceeds size
Best Practices
- Keep main bundle under 200KB (gzipped)
- Use dynamic imports for routes and heavy components
- Enable tree shaking with ES6 modules
- Analyze bundles regularly with webpack-bundle-analyzer
- Split vendor code from application code
- Use lighter alternatives to heavy libraries when possible
- Remove unused dependencies with
depcheck - Enable compression (gzip/brotli) on server
- Use
sideEffects: falsein package.json for libraries - Monitor bundle size in CI with tools like bundlesize