React Build Optimization
Focus: Webpack/Vite optimization, bundle analysis, and build performance
Question 1: How do you optimize React bundle size?β
Difficulty: π‘ Medium Frequency: βββββ Time: 12-15 minutes Companies: Meta, Google, Netflix, Amazon, Microsoft
Questionβ
What strategies can you use to reduce React bundle size? How do you implement tree-shaking, code splitting, and dependency optimization? What tools help analyze bundle composition?
Answerβ
Bundle Optimization Strategies:
- Tree-Shaking - Remove unused code at build time using ES modules
- Code Splitting - Split bundle into chunks loaded on-demand (route-based, component-based)
- Dynamic Imports - Load modules conditionally using
import()syntax - Dependency Analysis - Identify and remove unused/duplicate dependencies
- Minification - Compress code using Terser or esbuild
- Compression - Gzip/Brotli compression at runtime
- Source Maps - Optional production source maps
- Vendor Splitting - Separate third-party code from app code
- Lazy Loading - Load components/routes when needed
- Library Alternatives - Use lighter alternatives (date-fns vs moment, preact vs react)
Bundle Analysis Workflow:
- Run webpack-bundle-analyzer to visualize dependencies
- Identify large libraries taking excessive space
- Check for duplicate dependencies (npm dedupe)
- Monitor bundle size in CI/CD pipeline
- Set bundle size budgets to prevent regression
Code Exampleβ
// ============================================
// 1. DYNAMIC IMPORTS (Code Splitting)
// ============================================
// β Bad: Load everything upfront
import HeavyReport from './components/HeavyReport';
import Analytics from './components/Analytics';
function Dashboard() {
const [tab, setTab] = useState('overview');
return (
<div>
{tab === 'report' && <HeavyReport />}
{tab === 'analytics' && <Analytics />}
</div>
);
}
// β
Good: Load on demand
import { lazy, Suspense } from 'react';
const HeavyReport = lazy(() => import('./components/HeavyReport'));
const Analytics = lazy(() => import('./components/Analytics'));
function Dashboard() {
const [tab, setTab] = useState('overview');
return (
<div>
<Suspense fallback={<Spinner />}>
{tab === 'report' && <HeavyReport />}
{tab === 'analytics' && <Analytics />}
</Suspense>
</div>
);
}
// ============================================
// 2. WEBPACK CONFIGURATION
// ============================================
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
// Tree-shaking: Remove unused code
usedExports: true,
// Minification
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: { drop_console: true },
format: { comments: false },
},
}),
],
// Split vendor and app code
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
name: 'common',
},
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
priority: 20,
},
},
},
},
module: {
rules: [
{
test: /\.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new BundleAnalyzerPlugin(),
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
}),
],
};
// ============================================
// 3. PACKAGE.JSON OPTIMIZATION
// ============================================
{
"dependencies": {
// β Bad: moment.js is 67KB
"moment": "^2.29.0",
// β
Good: date-fns is 13KB (5x smaller)
"date-fns": "^2.30.0"
}
}
// In code:
import { format } from 'date-fns';
// Only imported functions are included (tree-shaked)
// ============================================
// 4. WEBPACK BUNDLE ANALYZER
// ============================================
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
generateStatsFile: true,
statsFilename: 'stats.json',
}),
],
};
// Run: webpack --mode production
// Output: Opens interactive bundle visualization
// ============================================
// 5. VITE OPTIMIZATION
// ============================================
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-library': ['@mui/material'],
'utils': ['lodash-es', 'date-fns'],
},
},
},
// Minify configuration
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
// Output analysis
reportCompressedSize: true,
},
});
// ============================================
// 6. IMPORT OPTIMIZATION
// ============================================
// β Bad: Imports entire library even if using 1 function
import _ from 'lodash';
const reversed = _.reverse(arr);
// Includes 30KB of lodash
// β
Good: Named imports for tree-shaking
import { reverse } from 'lodash-es';
const reversed = reverse(arr);
// Only 1KB included
// β
Better: Direct import (10x smaller)
import reverse from 'lodash-es/reverse';
const reversed = reverse(arr);
// 0.5KB
// ============================================
// 7. CONDITIONAL IMPORTS
// ============================================
// β Bad: Includes dev-only code in production
import devTools from './dev-tools'; // 50KB
function App() {
if (process.env.NODE_ENV === 'development') {
devTools.init();
}
return <div>App</div>;
}
// β
Good: Tree-shake dev-only code
if (process.env.NODE_ENV === 'development') {
require('./dev-tools'); // Only included in dev build
}
// β
Better: Dynamic import
async function initDevTools() {
if (process.env.NODE_ENV === 'development') {
const devTools = await import('./dev-tools');
devTools.init();
}
}
// ============================================
// 8. ROUTE-BASED CODE SPLITTING
// ============================================
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Admin = lazy(() => import('./pages/Admin'));
// Each route = separate bundle chunk
// Loaded only when user navigates there
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<Admin />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// ============================================
// 9. PACKAGE ANALYSIS SCRIPT
// ============================================
// npm-check-updates: Find unused dependencies
npm outdated
npm prune // Remove unused packages
// analyze-size: Detailed breakdown
npx package-size react react-dom lodash
// depcheck: Find unused imports
npx depcheck --ignores @types/node
// Output analysis:
// react: 42KB (gzipped)
// react-dom: 39KB (gzipped)
// lodash: 24KB (unused 15KB)
// ============================================
// 10. CI/CD BUNDLE SIZE MONITORING
// ============================================
// .github/workflows/bundle-check.yml
name: Bundle Size Check
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: npm run build
- name: Analyze
run: |
size=$(stat --printf="%s" dist/main.*.js | awk '{print $1 / 1024}')
echo "Bundle size: ${size}KB"
if (( $(echo "$size > 500" | bc -l) )); then
echo "Bundle too large (> 500KB)"
exit 1
fi
- name: Generate Report
run: webpack-bundle-analyzer dist/stats.json
- name: Upload to PR
uses: actions/upload-artifact@v3
with:
name: bundle-report
path: dist/bundle-report.html
Resourcesβ
- Webpack: Code Splitting
- webpack-bundle-analyzer
- Vite: Build Optimization
- Web.dev: JavaScript optimizations
- MDN: Using the Performance API
<details> <summary><strong>π Deep Dive</strong></summary>
Tree-Shaking Mechanics:
Tree-shaking requires ES modules (import/export, not CommonJS). Webpack's DCE (Dead Code Elimination) analyzes dependency graph through a multi-phase process that occurs during the compilation and optimization pipeline. Understanding this process is critical for senior engineers optimizing large-scale applications.
The Four-Phase Tree-Shaking Process:
-
Mark Phase - Webpack marks all exported symbols as potential exports
- Parses AST (Abstract Syntax Tree) of every module
- Identifies
exportstatements and creates export graph - Time complexity: O(n) where n = number of modules
- Example: 1000 modules with 5 exports each = 5000 exports marked
-
Analyze Phase - Tracks which exports are actually imported
- Walks dependency graph from entry point
- Marks imports as "used" references
- Detects side effects (mutations, global state changes)
- Time: ~500ms for medium apps (10k modules)
-
Eliminate Phase - Removes unused exports from bundle
- Webpack flags unused exports with
/* unused harmony export */ - Does NOT remove code yet (that's Terser's job)
- Only marks for removal in final step
- Webpack flags unused exports with
-
Minify Phase - Terser removes flagged dead code
- Removes unreachable code blocks
- Removes unused variables and functions
- Performs constant folding and expression simplification
- Time: ~2-5s for large apps (100k LOC)
Detailed Example with AST Analysis:
// math.js (source)
export function add(a, b) { return a + b; } // Used
export function subtract(a, b) { return a - b; } // Unused
export const PI = 3.14159; // Unused
// app.js
import { add } from './math'; // Only add imported
console.log(add(2, 3));
// === Webpack Analysis Steps ===
// Step 1: Mark Phase (AST parsed)
// math.js exports:
// - add (function declaration)
// - subtract (function declaration)
// - PI (const declaration)
// Step 2: Analyze Phase (dependency graph)
// Entry: app.js
// β imports { add } from './math'
// β add marked as USED
// β subtract marked as UNUSED
// β PI marked as UNUSED
// Step 3: Eliminate Phase (webpack output before minify)
/* harmony export */ export function add(a, b) { return a + b; }
/* unused harmony export subtract */
// function subtract(a, b) { return a - b; } // Commented by webpack
/* unused harmony export PI */
// const PI = 3.14159; // Commented by webpack
// Step 4: Minify Phase (Terser removes comments and dead code)
export function add(a,b){return a+b}
// subtract and PI completely removed from final bundle
Bundle Size Reduction Formula:
Final Size = (Source Code + Dependencies) - (Unused Code) - (Compression)
= Base JS - Tree-shaken - Minified - (Gzip/Brotli)
Real-world example breakdown:
Source files:
- React: 120KB (unminified)
- Redux: 30KB (unminified)
- Lodash: 72KB (unminified, 304 functions)
- App code: 200KB (unminified)
Total: 422KB
After tree-shaking:
- React: 120KB (all used, no reduction)
- Redux: 30KB (all used, no reduction)
- Lodash: 15KB (only 8 functions used, 79% removed)
- App code: 150KB (dead code removed, 25% reduction)
Total: 315KB (25% reduction from tree-shaking)
After minification:
Total: 190KB (40% reduction from minification)
After Gzip:
Total: 57KB (70% reduction from compression)
After Brotli:
Total: 47KB (75% reduction from compression)
Final savings: 422KB β 47KB (89% reduction!)
Webpack vs Vite Architecture Comparison:
| Aspect | Webpack | Vite | Impact |
|---|---|---|---|
| Dev Server Start | 15-30s | 300-500ms | 30-60x faster |
| Build Mechanism | Bundle entire app | ESM + esbuild | Different approach |
| HMR Speed | 2-5s | 50-200ms | 10-25x faster |
| Production Build | 20-45s | 5-15s | 3-4x faster |
| Bundle Size | Optimized | Similar (Rollup) | Comparable |
| Config Complexity | 100-300 lines | 10-50 lines | 3-10x simpler |
| Plugin Ecosystem | 10,000+ plugins | 500+ plugins | More mature |
| Memory Usage | 500MB-2GB | 100-300MB | 3-5x less |
| Watch Mode CPU | 25-40% (bundling) | 2-5% (on-demand) | 5-10x efficient |
Why Vite is Faster - Technical Deep Dive:
Webpack bundles EVERYTHING upfront:
Cold Start:
1. Parse all 1000 source files (5s)
2. Build complete dependency graph (3s)
3. Bundle into chunks (4s)
4. Transpile with Babel (8s)
5. Serve bundle (200ms)
Total: ~20 seconds
HMR (file change):
1. Detect change (50ms)
2. Rebuild affected modules (1s)
3. Rebuild dependency graph (500ms)
4. Re-bundle entire chunk (2s)
5. Send update to browser (100ms)
Total: ~3.6 seconds
Vite uses native ESM + esbuild:
Cold Start:
1. Pre-bundle dependencies (esbuild, 200ms)
2. Start dev server (100ms)
3. Serve source files as-is (no bundling)
4. Browser requests modules on-demand
Total: ~300ms
HMR (file change):
1. Detect change (20ms)
2. Transpile only changed file (30ms, esbuild is 10-100x faster than Babel)
3. Send update to browser (50ms)
4. Browser hot-swaps module
Total: ~100ms
Code Splitting Strategies with Performance Metrics:
- Route-Based Splitting (Optimal for SPAs)
// Each route = separate chunk
const Home = lazy(() => import('./pages/Home')); // 45KB
const Dashboard = lazy(() => import('./pages/Dashboard')); // 120KB
const Settings = lazy(() => import('./pages/Settings')); // 35KB
const Admin = lazy(() => import('./pages/Admin')); // 80KB
// Without splitting: 280KB initial load
// With splitting: 45KB initial + load others on navigation
// Improvement: 84% reduction in initial load
// User metrics:
// - FCP: 3.2s β 0.9s (72% faster)
// - TTI: 5.1s β 1.4s (73% faster)
// - Bounce rate: 35% β 12% (66% reduction)
- Component-Based Splitting (Heavy components only)
// Split components > 50KB
const DataGrid = lazy(() => import('./DataGrid')); // 150KB
const ChartEditor = lazy(() => import('./ChartEditor')); // 200KB
const PDFViewer = lazy(() => import('./PDFViewer')); // 180KB
// Load on-demand when user opens feature
// Dashboard without grids: 80KB β loads in 600ms
// Dashboard with grids: 230KB β loads in 1.8s
// But user only pays cost when using feature
- Vendor Splitting (Aggressive caching)
// webpack.config.js
splitChunks: {
cacheGroups: {
reactVendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
priority: 20,
// 42KB, changes rarely (cache for weeks)
},
uiLibrary: {
test: /[\\/]node_modules[\\/](@mui)[\\/]/,
name: 'ui-vendor',
priority: 15,
// 180KB, changes rarely
},
common: {
minChunks: 2,
priority: 5,
name: 'common',
// 25KB, shared utilities
},
},
}
// Cache efficiency:
// - react-vendor.js: Cached 30 days (99% cache hit rate)
// - app.js: Updated daily (0% cache hit rate)
// Net effect: User downloads React once, app code frequently
// Bandwidth saved: 42KB Γ 10 visits = 420KB saved per user
- Threshold-Based Splitting (Automatic optimization)
splitChunks: {
chunks: 'all',
maxSize: 150000, // 150KB max per chunk
minSize: 20000, // 20KB min per chunk
}
// Webpack auto-splits large chunks
// Before: main.js (450KB)
// After:
// - main.js (140KB)
// - chunk-vendors.js (150KB)
// - chunk-common.js (120KB)
// - chunk-utils.js (40KB)
// Parallel downloads = faster overall load
Compression Algorithms - Technical Comparison:
Gzip (Deflate Algorithm):
- Compression ratio: 60-75% for JavaScript
- Speed: 5-10 MB/s compression, 50-100 MB/s decompression
- Browser support: 99.9% (IE6+)
- CPU overhead: Low (browser decompresses in ~10ms for 100KB)
- Best for: Universal compatibility
Brotli (Modern Algorithm):
- Compression ratio: 70-80% for JavaScript (10-20% better than Gzip)
- Speed: 1-2 MB/s compression, 30-60 MB/s decompression
- Browser support: 95%+ (Chrome 50+, Firefox 44+, Safari 11+)
- CPU overhead: Slightly higher (browser decompresses in ~15ms for 100KB)
- Best for: Modern browsers, maximum compression
Real-world compression example:
// Original file: app.js (150KB uncompressed)
// Gzip compression:
app.js.gz β 45KB (70% reduction)
Transfer time (4G): 45KB Γ· 1MB/s = 45ms
Decompression: ~8ms
Total: 53ms
// Brotli compression:
app.js.br β 38KB (75% reduction)
Transfer time (4G): 38KB Γ· 1MB/s = 38ms
Decompression: ~12ms
Total: 50ms
// Difference: 3ms faster with Brotli
// On 3G (384 Kbps = 48 KB/s):
// - Gzip: 45KB Γ· 48KB/s = 937ms
// - Brotli: 38KB Γ· 48KB/s = 791ms
// Difference: 146ms faster (16% improvement)
// Recommendation: Serve both, let browser choose
Accept-Encoding: gzip, deflate, br
β Server responds with Brotli if supported
β Falls back to Gzip for older browsers
Dependency Duplication Problem (npm/yarn hell):
# Detect duplicates
$ npm ls lodash
myapp@1.0.0
βββ¬ redux-form@8.3.0
β βββ lodash@4.17.20
βββ¬ redux-thunk@2.3.0
β βββ lodash@4.17.21 # Different version!
βββ¬ react-table@7.7.0
βββ lodash@4.17.15 # Another version!
# Impact: 3 copies Γ 24KB = 72KB wasted
# Fix 1: npm dedupe (automatic)
$ npm dedupe
$ npm ls lodash
myapp@1.0.0
βββ lodash@4.17.21 # Single copy
# Fix 2: Webpack alias (force single version)
resolve: {
alias: {
lodash: path.resolve(__dirname, 'node_modules/lodash'),
},
}
# Fix 3: Use lodash-es (tree-shakable)
import { map, filter } from 'lodash-es';
// Only included functions bundled (3KB vs 24KB)
Lazy Loading Waterfall Optimization:
=== Without Optimization (Waterfall Problem) ===
0ms: main.js downloaded (50KB, 400ms on 3G)
400ms: React boots, app renders
500ms: User clicks "Dashboard"
500ms: dashboard.chunk.js starts downloading (120KB, 960ms on 3G)
1460ms: Dashboard renders
Total: 1460ms from click to render
=== With Prefetch Optimization ===
0ms: main.js downloaded (50KB, 400ms on 3G)
400ms: React boots, app renders
<link rel="prefetch" href="dashboard.chunk.js">
Browser downloads during idle time (low priority)
500ms: User clicks "Dashboard"
500ms: dashboard.chunk.js already in cache!
520ms: Dashboard renders (cache lookup ~20ms)
Total: 20ms from click to render (73x faster!)
=== With Preload (Even More Aggressive) ===
0ms: main.js downloaded (50KB)
<link rel="preload" href="dashboard.chunk.js" as="script">
Browser downloads immediately (high priority)
200ms: dashboard.chunk.js downloaded in parallel
400ms: React boots
500ms: User clicks "Dashboard"
500ms: Dashboard renders instantly (already loaded)
Total: 0ms from click to render (instant!)
// Trade-off: Preload increases initial bandwidth
// Use prefetch for likely routes (80% probability)
// Use preload for critical routes (100% probability)
Advanced Tree-Shaking with Side Effects:
// package.json optimization
{
"name": "my-lib",
"sideEffects": false // No side effects, safe to tree-shake
}
// OR specify files with side effects
{
"sideEffects": [
"*.css", // CSS has side effects (global styles)
"./src/polyfills.js" // Polyfills have side effects
]
}
// Impact on tree-shaking:
// Without sideEffects flag:
// - Webpack assumes every module has side effects
// - Cannot remove unused imports
// - Example: import 'lodash/map' pulls entire lodash
// With sideEffects: false:
// - Webpack knows it's safe to remove unused code
// - Tree-shaking removes 70-90% of unused library code
// - Example: import { map } from 'lodash-es' only includes map
</details>
<details> <summary><strong>π Real-World Scenario</strong></summary>
Problem: E-commerce site bundle bloated to 800KB (gzipped 250KB).
Symptoms:
- First contentful paint: 6.2 seconds (target: < 1.5s)
- Page load users drop 40% at 3 second mark
- Mobile (3G): 15+ seconds (user bounces)
- Core Web Vitals: Failed (LCP > 4s)
Root Cause Analysis:
# Step 1: Generate bundle analysis
npm run build && webpack-bundle-analyzer dist/stats.json
# Results:
- moment.js: 68KB (gzipped 23KB) - Only used for 2 dates
- chart.js: 150KB (gzipped 45KB) - Imported but unused
- lodash: 24KB (gzipped 4KB) - Full lib, used 3 functions
- React DevTools: 15KB (gzipped 5KB) - Left in production
- Source maps: 400KB (not compressed in HTTP)
- Duplicate Redux: 16KB (gzipped 3KB) - Two versions
Step-by-Step Fixes:
- Replace moment.js with date-fns:
// Before (68KB)
import moment from 'moment';
const formatted = moment().format('YYYY-MM-DD');
// After (8KB library, 3KB gzipped)
import { format } from 'date-fns';
const formatted = format(new Date(), 'yyyy-MM-dd');
// Saving: 60KB uncompressed, 20KB gzipped
- Remove unused chart.js:
// Identified in bundle analyzer but never imported in code
// Webpack couldn't tree-shake (mixed CJS/ESM)
// Solution: Remove package entirely, use lightweight alternative
// Before
import ChartJS from 'chart.js';
// After - Use recharts (already in deps, smaller)
import { LineChart, Line } from 'recharts';
// Saving: 150KB uncompressed, 45KB gzipped
- Optimize lodash imports:
// Before: Full lodash (24KB)
import _ from 'lodash';
_.map(arr, x => x * 2);
_.uniq(arr);
_.debounce(fn, 300);
// After: Named imports
import map from 'lodash-es/map';
import uniq from 'lodash-es/uniq';
import debounce from 'lodash-es/debounce';
// With tree-shaking: Only 8KB included
// Saving: 16KB uncompressed, 3KB gzipped
- Remove DevTools from production:
// webpack.config.js
{
plugins: [
process.env.NODE_ENV === 'development'
? new ReduxDevTools()
: null,
].filter(Boolean),
}
// Saving: 15KB uncompressed, 5KB gzipped
- Disable source maps in production:
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
devtool: process.env.NODE_ENV === 'development'
? 'source-map'
: false, // No source maps in prod
}
// Saving: 400KB uncompressed
- Implement code splitting by routes:
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));
// main.js: 80KB β 40KB (split to 4 chunks)
// User only downloads Checkout when needed
Results After Fixes:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Bundle Size (uncompressed) | 800KB | 280KB | 65% reduction |
| Bundle Size (gzipped) | 250KB | 85KB | 66% reduction |
| First Contentful Paint | 6.2s | 1.8s | 71% faster |
| Largest Contentful Paint | 8.5s | 2.1s | 75% faster |
| Cumulative Layout Shift | 0.15 (poor) | 0.03 (good) | 80% improvement |
| Mobile (3G) Load Time | 15.2s | 4.8s | 68% faster |
| Bounce Rate | 38% | 12% | 68% drop |
| Page Load Conversions | +0% | +45% | Business impact |
Metrics Tracked in Monitoring:
// Track bundle size in CI/CD
sizes = {
'main.js': 45KB,
'react-vendor.js': 40KB,
'pages/products.js': 120KB,
'pages/checkout.js': 75KB,
'total': 280KB
}
// Alert if any chunk grows > 10%
// Blocks PR if total > 320KB
</details>
<details> <summary><strong>βοΈ Trade-offs</strong></summary>
| Factor | Bundle Size | Load Time | Development | Complexity |
|---|---|---|---|---|
| Tree-Shaking | β¬οΈβ¬οΈ (removes unused) | β¬οΈ (smaller JS) | β Easy | β Low |
| Code Splitting | β (same total) | β¬οΈβ¬οΈ (parallel) | β οΈ More chunks | π΄ High |
| Lazy Loading | β (deferred) | β¬οΈβ¬οΈ (initial load) | π΄ More async | π΄ High |
| Minification | β¬οΈβ¬οΈ (30-40% smaller) | β¬οΈ (smaller JS) | β οΈ Debug harder | β Low |
| Compression (Gzip) | β¬οΈβ¬οΈβ¬οΈ (60-70%) | β¬οΈβ¬οΈ (network) | β Same code | β Zero |
| Brotli Compression | β¬οΈβ¬οΈβ¬οΈβ¬οΈ (70-75%) | β¬οΈβ¬οΈβ¬οΈ (best) | β Same code | β οΈ Setup |
| Library Alternation | β¬οΈβ¬οΈ (smaller lib) | β¬οΈ (smaller JS) | β οΈ Learn new API | β οΈ Medium |
| Vendor Splitting | β (same total) | β¬οΈ (caching) | β οΈ More chunks | β οΈ Medium |
| Remove DevTools | β¬οΈ (5-15% less) | β¬οΈ (less JS) | π΄ No debugging | π΄ High |
Decision Matrix:
When to use Tree-Shaking (Low Cost):
- β Always enabled by default
- β Requires ES modules only
- β No tradeoffs, only benefits
When to use Code Splitting (Medium Cost):
- β Routes are distinct (products, checkout)
- β Components are >50KB
- β οΈ Adds async loading complexity
- β οΈ Risk: More HTTP requests
When to use Compression (High Benefit, Zero Cost):
- β Always enable Gzip (browser default)
- β Enable Brotli if 85%+ browser support
- β No dev impact, server-side only
When to Lazy Load (Only if Needed):
- β Component user won't see immediately
- β οΈ Creates network waterfall
- β οΈ Adds loading state complexity
- β Don't lazy load critical path
Production Checklist:
- Gzip enabled on server
- Source maps disabled
- Tree-shaking verified
- Bundle < 200KB gzipped
- Vendor separately from app
- LCP < 2.5s on 4G
- CI/CD monitors bundle size
- Unused dependencies removed
</details>
<details> <summary><strong>π¬ Explain to Junior</strong></summary>
Bundle Optimization Like Shipping a Package:
Unoptimized (Wasteful): You're shipping a store full of items to customers.
- Customer ordered 2 shirts but you ship entire inventory (1000 shirts = 800KB)
- Takes 1 week to arrive (6.2 seconds)
- Customer waits so long they leave
Tree-Shaking (Removing Packaging Waste): Only pack what customer ordered.
- Remove 100 unused shirts β 800KB β 280KB
- Removes "packaging fluff" (unused code)
Code Splitting (Breaking into Smaller Boxes): Send different boxes as customer needs them.
- Main box (essential items): 40KB - Arrives immediately
- Product box: Arrives when customer browses products
- Checkout box: Arrives when customer checks out
- Customer never waits for entire inventory
Compression (Vacuum-Sealing): Compress boxes to save space.
- Gzip: Shrink by 60% (like vacuum sealing)
- Brotli: Shrink by 75% (better compression)
- Same contents, just smaller package
Library Alternatives (Choosing Smaller Tools):
- Moment.js = Full toolbox (68KB)
- date-fns = Just the screwdriver you need (8KB)
- Same job, 8x smaller
Interview Answer Template:
"I optimize React bundle size through multiple strategies:
-
Tree-Shaking - Use ES modules so Webpack removes unused code. For example, when I import only
formatfrom date-fns, unused functions are stripped from the bundle. -
Code Splitting - Split by routes. Home, Dashboard, and Settings are separate chunks, so users only download what they view. Using React.lazy() and route-based splitting reduces initial load from 250KB to 50KB.
-
Dependency Optimization - Replace heavy libraries. I switched from moment (68KB) to date-fns (8KB), saving 60KB. Used webpack-bundle-analyzer to identify unused dependencies.
-
Minification & Compression - Configure Webpack to minify with Terser and enable Gzip (60% reduction) or Brotli (75% reduction).
-
Monitoring - Set bundle size budget in CI/CD. Alert if main bundle exceeds 200KB gzipped. Track in Lighthouse and Core Web Vitals.
Real-world example: Reduced an e-commerce bundle from 800KB to 280KB (65% reduction), improving FCP from 6.2s to 1.8s and increasing conversions by 45%."
Common Follow-up Questions:
- "How do you handle large libraries you must use?" β Async load, separate bundle, tree-shake
- "What's the difference between tree-shaking and minification?" β Tree-shaking removes functions, minification compresses code
- "How do code splitting and lazy loading differ?" β Code splitting creates chunks, lazy loading defers their download
- "When should you split code?" β Routes, heavy components, feature flags
- "How do you prevent bundle regression?" β CI/CD size checks, automated alerts
</details>
Question 2: How do you analyze and improve build performance?β
Difficulty: π‘ Medium Frequency: ββββ Time: 10-12 minutes Companies: Meta, Google, Amazon, Shopify, Microsoft
Questionβ
How do you measure and improve React build performance? What tools and metrics do you use? How do you optimize webpack/Vite build speed? What's the difference between build time and runtime performance?
Answerβ
Build Performance Fundamentals:
-
Build Time - How long compilation takes (dev experience)
- Development: Should be < 2s for changes (HMR)
- Production: Should be < 5 minutes
-
Bundle Size - Final output size (runtime impact)
- Initial load: Should be < 200KB gzipped
- Per-chunk: < 150KB each
-
Runtime Performance - How fast app runs (UX)
- FCP (First Contentful Paint): < 1.5s
- LCP (Largest Contentful Paint): < 2.5s
-
Key Metrics to Track:
- Build time (seconds)
- Bundle size (KB)
- Number of modules/chunks
- Time spent in parsing/compilation
- Memory usage during build
Analysis Tools:
- webpack-bundle-analyzer (visualize bundle)
- speed-measure-webpack-plugin (build time breakdown)
- node --inspect (profile Node.js build process)
- Lighthouse (runtime performance)
- WebPageTest (real-world performance)
Code Exampleβ
// ============================================
// 1. SPEED MEASUREMENT PLUGIN
// ============================================
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const webpackConfig = {
entry: './src/index.js',
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
cacheDirectory: true, // Speed up babel
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
// Speed measure plugin shows time per loader/plugin
],
};
module.exports = smp.wrap(webpackConfig);
// Output:
// SMP β‘ [General output time]: 23.456 s
// SMP β‘ [babel-loader]: 12.345 s
// SMP β‘ [css-loader]: 4.123 s
// SMP β‘ [TerserPlugin]: 5.234 s
// SMP β‘ [other]: 1.754 s
// ============================================
// 2. WEBPACK CONFIGURATION OPTIMIZATION
// ============================================
module.exports = {
// 1. Use production mode (enables optimizations)
mode: 'production',
// 2. Parallel compilation
parallelism: 4, // Default is os.cpus().length
// 3. Cache builds (v5+, huge speed improvement)
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
// 4. Module federation (share code between apps)
// Reduces build time for monorepos
// 5. Exclude node_modules from compilation
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false, // Faster
},
},
},
],
},
// 6. Optimize dependencies
optimization: {
nodeEnv: 'production',
sideEffects: true, // Important for tree-shaking
usedExports: true,
concatenateModules: true, // Scope hoisting
},
// 7. Use esbuild (faster than Terser)
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4,
terserOptions: {
compress: {
drop_console: true,
},
},
}),
],
},
plugins: [
// 8. Avoid unnecessary plugins
// Remove BundleAnalyzer from production build
process.env.ANALYZE ? new BundleAnalyzerPlugin() : null,
// 9. Use ThreadLoader for expensive operations
// Only in Webpack dev config, not needed in prod
].filter(Boolean),
};
// ============================================
// 3. VITE OPTIMIZATION (Much Faster)
// ============================================
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
// 1. Optimize dependencies pre-bundling
optimizeDeps: {
include: ['react', 'react-dom', 'lodash-es'],
exclude: ['my-large-lib'], // Don't pre-bundle
},
// 2. Source map strategy
build: {
sourcemap: process.env.ANALYZE ? true : false,
// Production: No source maps (faster build, smaller)
// Analysis: Include for debugging
},
// 3. esbuild options (faster than webpack)
esbuild: {
drop: ['console', 'debugger'], // Remove logs
legalComments: 'none', // Remove license comments
},
// 4. Rollup configuration
build: {
rollupOptions: {
// Don't inline everything (smaller chunks)
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
},
},
},
},
});
// Build time comparison:
// Webpack: 45 seconds
// Vite: 8 seconds (5.6x faster!)
// ============================================
// 4. BUILD TIME MEASUREMENT SCRIPT
// ============================================
// measure-build.js
const { performance } = require('perf_hooks');
const { spawn } = require('child_process');
const start = performance.now();
spawn('webpack', ['--mode', 'production']).on('close', () => {
const duration = performance.now() - start;
console.log(`Build time: ${(duration / 1000).toFixed(2)}s`);
// Log to metrics service
const metrics = {
buildTime: duration,
timestamp: new Date().toISOString(),
branch: process.env.BRANCH,
};
fetch('https://metrics.example.com/build-time', {
method: 'POST',
body: JSON.stringify(metrics),
});
});
// package.json
{
"scripts": {
"build": "node measure-build.js",
"build:analyze": "ANALYZE=true webpack --mode production"
}
}
// ============================================
// 5. LIGHTHOUSE CI SETUP
// ============================================
// lighthouse-ci.json
{
"ci": {
"collect": {
"url": ["http://localhost:3000/"],
"numberOfRuns": 3,
"settings": {
"configPath": "./lighthouserc.json"
}
},
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
// CI/CD Pipeline
// 1. Build bundle
// 2. Run Lighthouse on production build
// 3. Compare against baseline
// 4. Fail if regression > 10%
// ============================================
// 6. WEBPACK BUNDLE ANALYSIS SCRIPT
// ============================================
// analyze-bundle.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const webpack = require('webpack');
const config = require('./webpack.config.js');
// Add analyzer plugin
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: process.env.OPEN !== 'false',
})
);
webpack(config, (err, stats) => {
if (err || stats.hasErrors()) {
console.error(stats.toJson().errors);
process.exit(1);
}
console.log(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}));
// Extract bundle size metrics
const assets = stats.toJson().assets;
let totalSize = 0;
let totalGzipped = 0;
assets.forEach(asset => {
totalSize += asset.size;
totalGzipped += asset.gzipSize || 0;
});
console.log(`\nTotal size: ${(totalSize / 1024).toFixed(2)}KB`);
console.log(`Total gzipped: ${(totalGzipped / 1024).toFixed(2)}KB`);
console.log(`Compression ratio: ${(100 - (totalGzipped / totalSize * 100)).toFixed(1)}%`);
});
// ============================================
// 7. DEVELOPMENT BUILD OPTIMIZATION
// ============================================
// webpack.dev.js
module.exports = {
mode: 'development',
// 1. Fast source maps for debugging
devtool: 'cheap-module-source-map',
// Alternative: eval-cheap-source-map (even faster)
// 2. Watch mode with --watch
// Only recompile changed files
// 3. Webpack dev server
devServer: {
hot: true, // Hot Module Replacement
port: 3000,
compress: false, // Skip compression in dev
client: {
overlay: true, // Show errors in browser
},
},
// 4. Skip optimization in dev
optimization: {
minimize: false, // Skip minification
usedExports: false, // Skip tree-shaking
},
// 5. Cache configuration
cache: {
type: 'memory', // Faster than filesystem in dev
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false, // 2x faster
},
},
},
],
},
};
// HMR (Hot Module Replacement) workflow:
// 1. User changes component
// 2. Webpack detects change (< 100ms)
// 3. Compiles changed module (~500ms)
// 4. Sends update to browser (< 100ms)
// 5. App rerenders without reload
// Total: ~700ms (vs 3-5s for full reload)
// ============================================
// 8. MONITORING BUILD METRICS
// ============================================
// .github/workflows/build-metrics.yml
name: Build Metrics
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build
run: |
time npm run build > /tmp/build.log 2>&1
wc -c dist/main.*.js | tail -1 > /tmp/bundle-size.txt
- name: Parse metrics
run: |
BUILD_TIME=$(grep real /tmp/build.log | awk '{print $2}')
BUNDLE_SIZE=$(cat /tmp/bundle-size.txt | awk '{print $1}')
echo "Build Time: $BUILD_TIME"
echo "Bundle Size: $((BUNDLE_SIZE / 1024))KB"
- name: Compare with baseline
run: |
# Store in artifact or database
# Alert if regression > 10%
- name: Generate HTML report
run: npm run analyze
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: bundle-report
path: dist/bundle-report.html
// ============================================
// 9. PERFORMANCE BUDGET
// ============================================
// performance-budget.json
{
"bundles": [
{
"name": "main",
"maxSize": "150kb"
},
{
"name": "vendor",
"maxSize": "100kb"
},
{
"name": "pages/*",
"maxSize": "75kb"
}
],
"metrics": [
{
"name": "FCP",
"maxValue": 1500
},
{
"name": "LCP",
"maxValue": 2500
}
]
}
// Enforce budget in CI/CD
// Fail build if:
// - main.js > 150KB
// - FCP > 1.5s
// - Any page chunk > 75KB
// ============================================
// 10. PROFILE NODE BUILD PROCESS
// ============================================
// npm run build -- --profile
// node --inspect ./node_modules/.bin/webpack
// In Chrome DevTools:
// 1. Visit chrome://inspect
// 2. Connect to Node.js process
// 3. Record profile during webpack build
// 4. Analyze call tree
// - Which loaders take longest?
// - Which files are slowest?
// - Memory leaks during build?
Resourcesβ
- Webpack: Build Performance
- Vite: Build Optimization
- Speed Measure Plugin
- Webpack Bundle Analyzer
- Lighthouse CI
- Web.dev: Performance
<details> <summary><strong>π Deep Dive</strong></summary>
Build Performance Components - The Complete Pipeline:
The webpack compilation pipeline has five distinct phases, each with specific optimization opportunities. Understanding these phases is essential for diagnosing and fixing build performance bottlenecks in production applications.
Phase 1: Entry Resolution (100-500ms)
- Locates entry files from webpack config
- Parses package.json to understand module structure
- Resolves aliases, extensions, and paths
- Creates initial module map
- Time: 100ms for simple apps, 500ms for complex monorepos with 50+ aliases
Optimization opportunities:
// β Slow: Many extensions to try
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.css', '.scss'],
// Webpack tries EACH extension for EVERY import
// 1000 imports Γ 7 extensions = 7000 file checks
}
// β
Fast: Minimal extensions, explicit imports
resolve: {
extensions: ['.js', '.jsx'], // Only what you use
// 1000 imports Γ 2 extensions = 2000 file checks
// 3.5x fewer checks
}
Phase 2: Module Compilation (1-5s, largest overhead)
- Each .js/.jsx file: ~1-5ms per file with Babel
- Transformation pipeline: Source β AST β Transformed AST β Code
- Babel is synchronous and CPU-intensive
- Example: 1000 files Γ 2ms = 2000ms (2 seconds)
- With Babel cache: 10 changed files Γ 2ms + 990 cache hits Γ 0.01ms = ~30ms (66x faster!)
Babel Transformation Breakdown:
// 1. Parse (source β AST): ~30% of time
const ast = babylon.parse(code, { sourceType: 'module' });
// 2. Transform (AST β modified AST): ~60% of time
const transformedAst = babel.transform(ast, {
presets: ['@babel/preset-react', '@babel/preset-env'],
plugins: ['@babel/plugin-proposal-class-properties'],
});
// Most expensive step (applies presets, plugins)
// 3. Generate (AST β code): ~10% of time
const output = generate(transformedAst, code);
// Total per file: ~2-5ms
// For 1000 files: 2-5 seconds
// Cache hit: ~0.01ms (200-500x faster)
Phase 3: Bundling/Linking (500ms-2s)
- Creates complete dependency graph
- Resolves module dependencies recursively
- Splits modules into chunks based on splitChunks config
- Performs tree-shaking (marks unused exports)
- Concatenates modules (scope hoisting)
Dependency Graph Construction:
// Simplified algorithm
function buildDependencyGraph(entryPoint) {
const graph = new Map();
const queue = [entryPoint];
const visited = new Set();
while (queue.length) {
const module = queue.shift();
if (visited.has(module)) continue;
visited.add(module);
const dependencies = parseDependencies(module); // ~1ms per module
graph.set(module, dependencies);
queue.push(...dependencies);
}
return graph;
}
// Time complexity: O(n) where n = number of modules
// For 1000 modules: ~1 second
// For 10,000 modules: ~10 seconds
Phase 4: Minification (1-3s, parallelizable)
- Terser processes all bundled code
- Removes whitespace, renames variables, removes dead code
- Single-threaded: 1 core processes entire bundle
- Parallel mode: Splits work across N CPU cores
Minification Impact:
// Input (readable): 150KB
function calculateTotal(items) {
const total = items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
return total;
}
// Output (minified): 95KB (37% reduction)
function calculateTotal(t){return t.reduce((t,e)=>t+e.price*e.quantity,0)}
// Terser processing time:
// - Single thread: 150KB Γ· 50KB/s = 3 seconds
// - 4 parallel threads: 150KB Γ· 200KB/s = 0.75 seconds (4x faster)
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4, // Use 4 CPU cores
terserOptions: {
compress: { drop_console: true }, // Remove console.log
},
}),
],
}
Phase 5: Emit (100-500ms)
- Writes compiled assets to disk
- Generates source maps (adds 40% overhead if enabled)
- Calculates content hashes for cache busting
- Creates manifest.json and stats.json
Emit Optimization:
// Source maps impact:
// Without source maps: 100ms write time
// With source maps: 140ms write time + 400KB extra size
// Recommendation: Conditional source maps
devtool: process.env.NODE_ENV === 'production'
? false // No source maps in prod
: 'cheap-module-source-map', // Fast source maps in dev
Complete Build Time Formula:
Total Build Time =
Entry Resolution (100-500ms) +
Module Compilation (1-5s, cacheable) +
Bundling/Linking (500ms-2s) +
Minification (1-3s, parallelizable) +
Emit (100-500ms) +
Disk I/O overhead (200-500ms)
Example for typical app (1000 modules):
= 200ms + 2s + 1s + 2s + 300ms + 300ms
= 5.8 seconds
With optimizations (cache + parallel):
= 200ms + 30ms + 1s + 500ms + 300ms + 300ms
= 2.3 seconds (2.5x faster!)
Webpack vs Vite - Architectural Deep Dive:
| Phase | Webpack | Vite | Why Vite Wins |
|---|---|---|---|
| Cold Start | 15-30s | 300-500ms | Vite doesn't bundle in dev |
| Dev HMR | 2-5s | 50-200ms | ESM updates single module |
| Prod Build | 20-45s | 5-15s | esbuild 10-100x faster than Babel |
| Mechanism | Bundle entire app | Native ESM + esbuild | No bundling overhead |
| Memory | 500MB-2GB | 100-300MB | No in-memory bundle |
Why Vite is Faster - Technical Breakdown:
Webpack's Approach (Traditional Bundling):
=== DEVELOPMENT MODE (webpack-dev-server) ===
Step 1: Parse all 1000 source files (5 seconds)
- Read from disk: 1000 Γ 2ms = 2s
- Parse to AST: 1000 Γ 1ms = 1s
- Transform (Babel): 1000 Γ 2ms = 2s
Step 2: Build complete dependency graph (3 seconds)
- Walk all imports recursively
- Resolve 5000 import statements
- Create module map
Step 3: Bundle into chunks (4 seconds)
- Concatenate modules
- Apply tree-shaking
- Generate bundle code
Step 4: Transpile and optimize (8 seconds)
- Babel transforms JSX
- Apply plugins
- Generate source maps
Step 5: Serve bundle (200ms)
- Write to memory
- Serve via express
TOTAL COLD START: ~20 seconds
=== HMR UPDATE (file change) ===
Step 1: Detect change (50ms)
Step 2: Rebuild affected modules (1s)
- Re-parse changed file
- Re-transform with Babel
Step 3: Rebuild dependency graph (500ms)
- Update graph edges
- Recalculate chunk
Step 4: Re-bundle affected chunk (2s)
- Concatenate modules again
- Tree-shake again
Step 5: Send update to browser (100ms)
TOTAL HMR: ~3.6 seconds
Vite's Approach (ESM + esbuild):
=== DEVELOPMENT MODE (Vite dev server) ===
Step 1: Pre-bundle dependencies ONCE (200ms)
- Use esbuild to bundle node_modules
- React + React-DOM: 200ms (vs 8s with webpack)
- Cached for future runs
Step 2: Start dev server (100ms)
- Lightweight Koa server
- No bundling step
Step 3: Serve source files as-is (instant)
- Browser requests /src/App.jsx
- Vite transforms ONLY that file on-demand
- Returns ESM module
Step 4: Browser loads modules on-demand
- Native import() support
- Only loads what's needed
TOTAL COLD START: ~300ms (67x faster than webpack!)
=== HMR UPDATE (file change) ===
Step 1: Detect change (20ms)
Step 2: Transform ONLY changed file (30ms)
- esbuild transforms in 30ms (vs 2s Babel)
- 66x faster than Babel
Step 3: Send precise update to browser (50ms)
- Browser hot-swaps single module
- No chunk rebuild needed
TOTAL HMR: ~100ms (36x faster than webpack!)
esbuild vs Babel Performance:
// Transform 1000 JSX files:
// Babel (JavaScript, single-threaded):
// - Time: 2-5 seconds
// - Speed: 200-500 files/second
// esbuild (Go, multi-threaded):
// - Time: 30-100ms
// - Speed: 10,000-30,000 files/second
// - 50-100x faster than Babel!
// Why esbuild is faster:
// 1. Written in Go (compiled, not interpreted)
// 2. Parallelizes across all CPU cores
// 3. Optimized AST representation
// 4. No plugin overhead
Babel Caching Deep Dive:
// Without cache (fresh build):
const start = performance.now();
for (const file of allFiles) { // 1000 files
const code = fs.readFileSync(file);
const ast = babel.parse(code); // ~1ms
const transformed = babel.transform(ast, babelConfig); // ~1ms
const output = babel.generate(transformed); // ~0.5ms
// Total per file: ~2.5ms
}
const duration = performance.now() - start;
// Total: 1000 Γ 2.5ms = 2500ms (2.5 seconds)
// With cache (incremental build):
// Scenario: User changes 10 files, 990 unchanged
for (const file of allFiles) {
const cacheKey = getCacheKey(file); // File hash
if (cache.has(cacheKey)) {
const output = cache.get(cacheKey); // ~0.01ms disk read
// Total: 0.01ms (250x faster!)
} else {
// Transform and cache (as above, ~2.5ms)
const output = transformWithBabel(file);
cache.set(cacheKey, output);
}
}
// Total: (10 Γ 2.5ms) + (990 Γ 0.01ms) = 25ms + 10ms = 35ms
// Improvement: 2500ms β 35ms (71x faster!)
Module Federation for Monorepos (Advanced):
Module Federation allows multiple webpack builds to share code at runtime, avoiding duplicate builds in monorepos.
// === MONOREPO SETUP ===
// apps/admin/webpack.config.js
{
plugins: [
new ModuleFederationPlugin({
name: 'admin',
filename: 'remoteEntry.js',
exposes: {
'./UserTable': './src/components/UserTable',
'./Dashboard': './src/components/Dashboard',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
}
// apps/customer/webpack.config.js
{
plugins: [
new ModuleFederationPlugin({
name: 'customer',
remotes: {
admin: 'admin@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
}
// apps/customer/src/App.jsx
import React, { lazy } from 'react';
// Load UserTable from admin app at RUNTIME
const UserTable = lazy(() => import('admin/UserTable'));
function App() {
return <UserTable />;
}
// === BUILD TIME SAVINGS ===
// Without Module Federation:
// - admin build: 12 seconds (includes UserTable)
// - customer build: 15 seconds (ALSO includes UserTable)
// - Total: 27 seconds
// - UserTable built TWICE (wasted effort)
// With Module Federation:
// - admin build: 12 seconds (includes UserTable)
// - customer build: 8 seconds (references admin's UserTable, doesn't rebuild it)
// - Total: 20 seconds (26% faster!)
// - UserTable built ONCE, shared at runtime
Profiling Webpack with Node Inspector:
# Terminal 1: Start webpack with Node debugger
$ node --inspect-brk ./node_modules/.bin/webpack --mode production
Debugger listening on ws://127.0.0.1:9229
For help, see: https://nodejs.org/en/docs/inspector
# Terminal 2: Open Chrome DevTools
# Navigate to chrome://inspect
# Click "inspect" next to the Node.js process
# In DevTools:
# 1. Go to "Profiler" tab
# 2. Click "Start"
# 3. Resume execution (F8)
# 4. Wait for build to finish
# 5. Click "Stop"
# === PROFILER RESULTS ===
# Top functions by self time:
# 1. babel-loader/transform: 2600ms (65% of total)
# β Bottleneck: Babel is slow
# 2. sass-loader/compile: 600ms (15% of total)
# β Compiling Sass files
# 3. TerserPlugin/minify: 480ms (12% of total)
# β Single-threaded minification
# 4. webpack/resolve: 320ms (8% of total)
# β Module resolution overhead
# === OPTIMIZATION ACTIONS ===
# 1. Enable Babel cache β 2600ms to 30ms (86x faster)
# 2. Parallel Terser β 480ms to 120ms (4x faster)
# 3. Simplify resolve.extensions β 320ms to 100ms (3x faster)
# Total: 4000ms β 850ms (4.7x faster!)
Bundle Size Metrics - Comprehensive Analysis:
// webpack stats.json output:
{
"assets": [
{
"name": "main.abc123.js",
"size": 150000, // 150KB uncompressed
"gzipSize": 45000, // 45KB gzipped (70% reduction)
"brotliSize": 38000 // 38KB brotli (75% reduction)
}
],
"modules": [
{
"name": "./src/App.jsx",
"size": 3500, // 3.5KB
"reasons": [...] // Why included
},
{
"name": "./node_modules/lodash/index.js",
"size": 72000, // 72KB (!!!)
"reasons": [
{ "type": "import", "userRequest": "lodash" }
]
// Problem: Entire lodash imported for 1 function
}
]
}
// === OPTIMIZATION OPPORTUNITIES ===
// 1. Tree-shaking lodash:
// Before: 72KB (entire library)
// After: 3KB (only used functions)
// Savings: 69KB (96% reduction)
// 2. Gzip compression:
// Before: 150KB uncompressed
// After: 45KB gzipped
// Savings: 105KB (70% reduction)
// 3. Brotli compression:
// Before: 150KB uncompressed
// After: 38KB brotli
// Savings: 112KB (75% reduction)
// 4. Combined optimizations:
// Original: 150KB uncompressed
// Tree-shaken: 81KB (150 - 69)
// Brotli: 20KB (81 Γ 0.25)
// Final savings: 130KB (87% reduction!)
</details>
<details> <summary><strong>π Real-World Scenario</strong></summary>
Problem: Build pipeline taking 8 minutes, blocking CI/CD. Developers frustrated with 5-second HMR delays.
Initial Diagnosis:
# Measure build time
$ npm run build
webpack 5.0.0 compiled with warnings in 480000ms (8 minutes!)
# What's taking so long?
$ webpack --profile --json > stats.json
$ webpack-bundle-analyzer stats.json
Results:
- Babel compilation: 280 seconds (58%)
- Minification (Terser): 120 seconds (25%)
- Webpack operations: 80 seconds (17%)
Root Causes Found:
- No Babel Caching:
// webpack.config.js (BEFORE)
{
test: /\.jsx?$/,
use: 'babel-loader',
}
// Every build recompiles 1000 files
// (AFTER)
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false, // Avoid compression overhead
},
},
}
// Impact: 280s β 15s (18x faster)
// From 90% CPU to 20% CPU during Babel phase
- Serial Minification (Single-threaded):
// webpack.config.js (BEFORE)
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
}
// Terser uses 1 CPU core
// (AFTER)
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: 4, // Use 4 CPU cores
}),
],
}
// Impact: 120s β 35s (3.4x faster)
// From 25% CPU usage to 90% CPU (utilizing all cores)
- Unnecessary Source Maps in CI:
// webpack.config.js (BEFORE)
devtool: 'source-map', // Always generated
// Adds 400KB + 40s computation time
// (AFTER)
devtool: process.env.CI ? false : 'source-map',
// Only in local development
// Impact: 40s saved
- Missing Webpack Cache (v5+):
// webpack.config.js (BEFORE)
// No cache configuration
// Every build parses all modules from scratch
// (AFTER)
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
}
// Stores compiled modules to disk
// Impact:
// - First build: 475s (no change)
// - Second build: 8s (95% faster!)
// - Incremental: 2-3s (only changed files)
- Inefficient Code Splitting:
// webpack.config.js (BEFORE)
optimization: {
splitChunks: {
chunks: 'all',
minSize: 30000,
}
}
// Creates 47 chunks, webpack spends time optimizing
// (AFTER)
optimization: {
splitChunks: {
chunks: 'async', // Only split dynamic imports
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
},
},
}
// Creates 5 chunks only
// Impact: Webpack linking phase 15s β 2s
Performance Fixes Applied:
| Fix | Before | After | Improvement |
|---|---|---|---|
| Babel caching | 280s | 15s | 18.6x |
| Parallel Terser | 120s | 35s | 3.4x |
| Remove source maps | 40s | 0s | - |
| Webpack cache | 8m (rebuilds) | 8s | 60x |
| Code split simplification | 15s | 2s | 7.5x |
| Total | 8 min | 30s (first build) / 8s (incremental) | 16x |
Final Build Metrics:
# Production builds
$ npm run build
webpack compiled in 32 seconds
ββ main: 45KB (gzipped)
ββ vendor: 40KB (gzipped)
ββ pages: 120KB (gzipped)
ββ Total: 205KB (gzipped)
# HMR during development
$ npm run dev
webpack compiled in 2.3s
β
App ready at localhost:3000
# File save β HMR update
$ Edit src/Button.jsx
webpack compiled in 380ms
β
Module updated (no page reload)
CI/CD Pipeline Impact:
Before:
- Build job: 8m 45s
- Lighthouse: 2m 30s
- Tests: 5m 20s
- Total: 16m 35s
- Blocker: Developers wait 16+ minutes per commit
After:
- Build job: 30s
- Lighthouse: 2m 30s
- Tests: 5m 20s
- Total: 8m 20s
- Result: 50% faster feedback loop
Developer Experience Improvement:
Before (5s HMR delay):
Edit component
β (wait 5s)
See change in browser
After (380ms HMR):
Edit component
β (instant, 380ms)
See change in browser
Developer can work in flow state
(no context-switching to wait for tools)
</details>
<details> <summary><strong>βοΈ Trade-offs</strong></summary>
| Optimization | Build Time | Dev HMR | Bundle Size | Complexity | Risk |
|---|---|---|---|---|---|
| Babel Cache | β¬οΈβ¬οΈβ¬οΈ (18x) | β¬οΈβ¬οΈβ¬οΈ | β | β Low | β Low |
| Parallel Minify | β¬οΈβ¬οΈ (3x) | β | β | β Low | β Low |
| Webpack Cache | β¬οΈβ¬οΈβ¬οΈ (60x rebuild) | β | β | β Low | β οΈ Can stale |
| Remove Dev Tools | β¬οΈ | β | β¬οΈβ¬οΈ | β Low | π΄ Less debugging |
| Code Splitting | β¬οΈ | β¬οΈ | β (chunks) | β οΈ Medium | β οΈ Complexity |
| Switch to Vite | β¬οΈβ¬οΈβ¬οΈ | β¬οΈβ¬οΈβ¬οΈβ¬οΈ | β | π΄ High | π΄ Ecosystem risk |
| Skip minification dev | β¬οΈβ¬οΈβ¬οΈ | β¬οΈβ¬οΈβ¬οΈ | β¬οΈβ¬οΈ (local only) | β Low | β Low |
Decision Framework:
Fast-win optimizations (do these first):
- β Babel caching (~10min β 1min)
- β Parallel minification (~2min faster)
- β Webpack cache (rebuilds only ~10s)
- β Skip source maps in CI
Medium-effort optimizations:
- β οΈ Simplify code splitting
- β οΈ Profile to find bottlenecks
- β οΈ Upgrade webpack/tools to latest
Large-effort optimizations (only if critical):
- π΄ Migrate from Webpack to Vite (weeks of work)
- π΄ Module federation for monorepos
- π΄ Custom build tool (rare, unless Meta-scale)
Build Time Targets:
| Stage | Target | Good | Acceptable | Poor |
|---|---|---|---|---|
| First build | < 30s | < 30s | 30-60s | > 60s |
| Incremental build (HMR) | < 1s | < 500ms | 500ms-2s | > 2s |
| CI/CD build | < 2m | < 1m | 1-3m | > 3m |
| Bundle size (gzipped) | < 150KB | < 150KB | 150-300KB | > 300KB |
When to Optimize Build Performance:
| Symptom | Action |
|---|---|
| Developers waiting 5+ seconds for HMR | Urgent - Babel cache, skip minification |
| CI/CD taking 10+ minutes | Urgent - Parallel builds, webpack cache |
| Bundle size > 500KB gzipped | Urgent - Tree-shake, code split, analyze |
| Build times slowly increasing | Proactive - Measure, set budget, monitor |
| Build works but slow | Defer - If cycle < 5s and bundle < 200KB |
</details>
<details> <summary><strong>π¬ Explain to Junior</strong></summary>
Build Optimization Like Factory Assembly:
Unoptimized Factory:
- Each worker does every step sequentially
- Product takes 8 minutes
- Workers frustrated, coffee cold before next product
Optimized Factory (Parallel Processing):
- Worker 1: Cuts wood (2 min)
- Worker 2: Stains wood (2 min)
- Worker 3: Assembles (2 min)
- Worker 4: Packaging (2 min)
- All 4 work simultaneously β Final product in 2 minutes!
Applying to Webpack:
-
Babel Caching = Remembering Completed Work
- First time cutting wood: 5 minutes
- Second time: "I remember this wood pattern!" β 10 seconds
- Webpack remembers compiled JavaScript
-
Parallel Minification = Multiple Workers
- 1 worker: 2 minutes (minifying slowly)
- 4 workers: 30 seconds (4x faster, same job)
- Use
parallel: 4in Terser config
-
Webpack Cache = Inventory System
- Without cache: "Is this module changed? Let me check... compiling..."
- With cache: "Yep, it's the same, here's the cached version" (disk lookup)
- 60 times faster on incremental builds
-
Code Splitting = Breaking into Smaller Boxes
- Ship whole factory output at once: 8 minutes
- Ship only what customer needs today: 2 minutes
- Bundle size smaller = faster network
Interview Answer Template:
"Build performance has two aspects: build time and bundle size.
Build Time Optimization:
First, I profile the build. Using speed-measure-webpack-plugin, I found Babel was taking 60% of time. I enabled cacheDirectory: true on babel-loader, which reduced build time from 8 minutes to 15 seconds by caching compiled modules.
Second, I parallelize. Terser was running single-threaded. Adding parallel: 4 reduced minification from 120s to 35s.
Third, I enable Webpack's cache (v5+). The first build is the same, but rebuilds drop from 8 minutes to 8 seconds because Webpack remembers what it already compiled.
Bundle Size Optimization: I use webpack-bundle-analyzer to visualize dependencies. Found moment.js was 68KB for 2 date formats. Switched to date-fns, saving 60KB.
I implement code splitting by routes. Home, Dashboard, Settings become separate chunks. Users only download the page they're viewing.
Monitoring: In CI/CD, I set bundle size budget. If main.js exceeds 150KB, the build fails. Run Lighthouse on each PR to catch performance regressions.
Result: 8-minute builds became 30 seconds. HMR improved from 5 seconds to 380ms, improving developer experience significantly."
Key Takeaways for Interview:
- Always measure first - Profile before optimizing (speed-measure-webpack-plugin)
- Target the bottleneck - Fix the slowest part (usually Babel or minification)
- Parallelize everything - Use all CPU cores (parallel workers in Terser)
- Cache aggressively - Babel cache and Webpack cache are huge wins
- Monitor in CI/CD - Set size budgets to prevent regression
- Know your tools - Webpack vs Vite tradeoffs, when to use each
</details>
Source: build-01-optimization.md