Bundle Optimization — Reducing SPA Bundle Size for Faster Loads
In this tutorial, you will learn about Bundle Optimization. We cover key concepts, practical examples, and best practices to help you master this topic.
Bundle optimization reduces JavaScript bundle size through Tree Shaking, dead code elimination, Minification, and dependency analysis to speed up SPA loading.
What You'll Learn
By the end of this tutorial, you will understand how to analyze bundle composition, eliminate unused code, optimize dependencies, and configure your build tool for minimal output.
Why It Matters
Every kilobyte of JavaScript costs about 1KB of download + 1ms of parse + 0.5ms of execution on mobile. A 400KB bundle takes 3+ seconds on slow 3G. Bundle optimization can cut this in half with no visible changes to the user.
Real-World Use
A dashboard SPA had a 650KB bundle. Analysis revealed: moment.js (230KB unused) replaced with date-fns (15KB), lodash full import (70KB) replaced with tree-shaken imports (8KB), and a large chart library replaced with a lighter alternative. Final bundle: 180KB, 72% reduction.
Bundle Analysis
// 1. Use webpack-bundle-analyzer (Webpack)
// 2. Use vite-bundle-analyzer (Vite)
// 3. Use source-map-explorer (any build)
// Programmatic analysis
import { join } from 'path';
import { readdirSync, statSync } from 'fs';
function analyzeDist(dir) {
const files = readdirSync(dir);
let totalSize = 0;
const largeFiles = [];
files.forEach(file => {
if (file.endsWith('.js') || file.endsWith('.css')) {
const stats = statSync(join(dir, file));
const sizeKB = stats.size / 1024;
totalSize += stats.size;
if (sizeKB > 50) {
largeFiles.push({ file, size: sizeKB.toFixed(1) });
}
}
});
console.log(`Total JS/CSS size: ${(totalSize / 1024).toFixed(1)}KB`);
console.log('Large files (>50KB):');
largeFiles.forEach(f => console.log(` ${f.file}: ${f.size}KB`));
}
Tree Shaking
// BAD: imports entire lodash library (~70KB)
import _ from 'lodash';
_.debounce(fn, 300);
_.throttle(fn, 100);
_.uniqBy(array, 'id');
// GOOD: tree-shaken imports (~3KB)
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
import uniqBy from 'lodash/uniqBy';
// Or use lodash-es for tree-shaking
import { debounce, throttle } from 'lodash-es';
// BAD: imports entire library
import * as dateUtils from 'date-fns';
// GOOD: imports only what is used
import { format, parseISO, differenceInDays } from 'date-fns';
Dependency Optimization
// package.json — Check for heavy dependencies
{
"dependencies": {
// Replace moment (230KB) with date-fns (15KB)
"date-fns": "^3.0.0",
// Replace full lodash (70KB) with specific imports
// Use UI library with tree-shaking
"@mui/material": "^5.15.0",
// Ensure ESM build for tree-shaking
"my-heavy-lib": "^2.0.0"
}
}
// Check dependency size before adding
// npm install — how big is this package?
// Use bundlephobia.com to check sizes
// Alternative lighter libraries:
// moment → date-fns or dayjs (88% smaller)
// lodash → specific methods or native Array methods
// jQuery → vanilla JS or framework-specific solutions
// FullCalendar → custom calendar component
// Three.js → specific exporters only
Code Elimination
// Webpack: enable tree-shaking in production mode
// module.exports = { mode: 'production' };
// Mark side-effect-free packages in package.json
{
"sideEffects": false
// or specific files that have side effects
// "sideEffects": ["./src/styles.css"]
}
// Remove dead code:
// - Delete unused components
// - Remove commented-out code
// - Remove unused exports
// - Consolidate duplicate utilities
// Use ESLint with no-unused-vars to catch dead code
// Use TypeScript with noUnusedLocals: true
Bundle Optimization Techniques
// 1. Replace heavy dependencies
// moment (230KB) → date-fns (15KB)
// lodash (70KB) → lodash-es tree-shaken
// jQuery (87KB) → vanilla JS
// 2. Defer non-critical CSS
// Critical CSS inlined in <head>
// Non-critical CSS loaded with:
<link rel="preload" href="styles.css" as="style">
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
// 3. Compress images
// Use WebP/AVIF format
// Responsive images with srcset
// Lazy load below-fold images
// 4. Minimize polyfills
// Use @babel/preset-env with targets
// Load polyfills only when needed:
if (!('IntersectionObserver' in window)) {
await import('intersection-observer-polyfill');
}
Common Mistakes
- Not analyzing the bundle before optimizing. Guessing what is large leads to wasted effort. Always run bundle analysis first.
- Replacing a library without verifying functionality. date-fns and moment have different APIs. Check all usage sites before replacing. 3.** Ignoring gzip/brotli compression sizes.** A 200KB gzipped bundle is very different from 200KB uncompressed. Measure compressed sizes.
- Over-optimizing small gains. Spending 2 days to save 2KB is not worth it. Focus on large wins (50KB+ reductions) first.
- Forgetting about CSS and image bundles. Bundle optimization is not just JavaScript. CSS and images are often larger than JS.
Practice Questions
- How do you analyze what is in your JavaScript bundle?
- What is tree shaking and how do you enable it?
- How do you identify and replace heavy dependencies?
- What is the difference between gzipped and uncompressed bundle size?
- How do you eliminate dead code from your bundle?
Challenge: Take an existing SPA (or a sample project). Analyze the bundle with Webpack-bundle-analyzer. Identify 3 optimization opportunities (heavy dependency, unused code, duplicate modules). Implement each optimization and measure the size reduction.
FAQ
Mini Project
Optimize a sample SPA bundle: install webpack-bundle-analyzer, run analysis, identify 3 optimization opportunities (replace moment with date-fns, tree-shake lodash, remove unused components), implement each, and compare before/after sizes. Target: 50% reduction.
What's Next
Your bundle is optimized. Now tackle SPA SEO challenges — making your single-page application discoverable by search engines.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro