Skip to main content

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:

  1. Tree-Shaking - Remove unused code at build time using ES modules
  2. Code Splitting - Split bundle into chunks loaded on-demand (route-based, component-based)
  3. Dynamic Imports - Load modules conditionally using import() syntax
  4. Dependency Analysis - Identify and remove unused/duplicate dependencies
  5. Minification - Compress code using Terser or esbuild
  6. Compression - Gzip/Brotli compression at runtime
  7. Source Maps - Optional production source maps
  8. Vendor Splitting - Separate third-party code from app code
  9. Lazy Loading - Load components/routes when needed
  10. 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​


<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:

  1. Mark Phase - Webpack marks all exported symbols as potential exports

    • Parses AST (Abstract Syntax Tree) of every module
    • Identifies export statements and creates export graph
    • Time complexity: O(n) where n = number of modules
    • Example: 1000 modules with 5 exports each = 5000 exports marked
  2. 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)
  3. 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
  4. 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:

AspectWebpackViteImpact
Dev Server Start15-30s300-500ms30-60x faster
Build MechanismBundle entire appESM + esbuildDifferent approach
HMR Speed2-5s50-200ms10-25x faster
Production Build20-45s5-15s3-4x faster
Bundle SizeOptimizedSimilar (Rollup)Comparable
Config Complexity100-300 lines10-50 lines3-10x simpler
Plugin Ecosystem10,000+ plugins500+ pluginsMore mature
Memory Usage500MB-2GB100-300MB3-5x less
Watch Mode CPU25-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:

  1. 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)
  1. 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
  1. 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
  1. 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:

  1. 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
  1. 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
  1. 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
  1. Remove DevTools from production:
// webpack.config.js
{
plugins: [
process.env.NODE_ENV === 'development'
? new ReduxDevTools()
: null,
].filter(Boolean),
}

// Saving: 15KB uncompressed, 5KB gzipped
  1. 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
  1. 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:

MetricBeforeAfterImprovement
Bundle Size (uncompressed)800KB280KB65% reduction
Bundle Size (gzipped)250KB85KB66% reduction
First Contentful Paint6.2s1.8s71% faster
Largest Contentful Paint8.5s2.1s75% faster
Cumulative Layout Shift0.15 (poor)0.03 (good)80% improvement
Mobile (3G) Load Time15.2s4.8s68% faster
Bounce Rate38%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>

FactorBundle SizeLoad TimeDevelopmentComplexity
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:

  1. Tree-Shaking - Use ES modules so Webpack removes unused code. For example, when I import only format from date-fns, unused functions are stripped from the bundle.

  2. 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.

  3. Dependency Optimization - Replace heavy libraries. I switched from moment (68KB) to date-fns (8KB), saving 60KB. Used webpack-bundle-analyzer to identify unused dependencies.

  4. Minification & Compression - Configure Webpack to minify with Terser and enable Gzip (60% reduction) or Brotli (75% reduction).

  5. 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:

  1. Build Time - How long compilation takes (dev experience)

    • Development: Should be < 2s for changes (HMR)
    • Production: Should be < 5 minutes
  2. Bundle Size - Final output size (runtime impact)

    • Initial load: Should be < 200KB gzipped
    • Per-chunk: < 150KB each
  3. Runtime Performance - How fast app runs (UX)

    • FCP (First Contentful Paint): < 1.5s
    • LCP (Largest Contentful Paint): < 2.5s
  4. 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​


<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:

PhaseWebpackViteWhy Vite Wins
Cold Start15-30s300-500msVite doesn't bundle in dev
Dev HMR2-5s50-200msESM updates single module
Prod Build20-45s5-15sesbuild 10-100x faster than Babel
MechanismBundle entire appNative ESM + esbuildNo bundling overhead
Memory500MB-2GB100-300MBNo 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:

  1. 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
  1. 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)
  1. 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
  1. 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)
  1. 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:

FixBeforeAfterImprovement
Babel caching280s15s18.6x
Parallel Terser120s35s3.4x
Remove source maps40s0s-
Webpack cache8m (rebuilds)8s60x
Code split simplification15s2s7.5x
Total8 min30s (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>

OptimizationBuild TimeDev HMRBundle SizeComplexityRisk
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:

StageTargetGoodAcceptablePoor
First build< 30s< 30s30-60s> 60s
Incremental build (HMR)< 1s< 500ms500ms-2s> 2s
CI/CD build< 2m< 1m1-3m> 3m
Bundle size (gzipped)< 150KB< 150KB150-300KB> 300KB

When to Optimize Build Performance:

SymptomAction
Developers waiting 5+ seconds for HMRUrgent - Babel cache, skip minification
CI/CD taking 10+ minutesUrgent - Parallel builds, webpack cache
Bundle size > 500KB gzippedUrgent - Tree-shake, code split, analyze
Build times slowly increasingProactive - Measure, set budget, monitor
Build works but slowDefer - 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:

  1. Babel Caching = Remembering Completed Work

    • First time cutting wood: 5 minutes
    • Second time: "I remember this wood pattern!" β†’ 10 seconds
    • Webpack remembers compiled JavaScript
  2. Parallel Minification = Multiple Workers

    • 1 worker: 2 minutes (minifying slowly)
    • 4 workers: 30 seconds (4x faster, same job)
    • Use parallel: 4 in Terser config
  3. 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
  4. 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:

  1. Always measure first - Profile before optimizing (speed-measure-webpack-plugin)
  2. Target the bottleneck - Fix the slowest part (usually Babel or minification)
  3. Parallelize everything - Use all CPU cores (parallel workers in Terser)
  4. Cache aggressively - Babel cache and Webpack cache are huge wins
  5. Monitor in CI/CD - Set size budgets to prevent regression
  6. Know your tools - Webpack vs Vite tradeoffs, when to use each

</details>


← Back to React README


Source: build-01-optimization.md