Build Tools — Build Optimization
Build optimization is the practice of transforming your source code and assets into the most efficient form for production deployment. A well-optimized build can reduce bundle sizes by 60-80%, improve load times by multiple seconds, and significantly reduce bandwidth costs.
Modern build pipelines combine multiple optimization techniques: minification removes unnecessary characters, compression reduces transfer size, tree-shaking eliminates dead code, and asset hashing enables aggressive caching. Understanding each technique and how they compose is essential for production-grade applications.
Minification removes unnecessary characters from source code — whitespace, comments, newlines, and shortens variable names — without changing functionality. Modern minifiers also perform dead code elimination and constant folding.
| 1 | // Before minification (3.2 KB) |
| 2 | function calculateTotal(items, taxRate, discountPercentage) { |
| 3 | // Calculate the subtotal by summing all item prices |
| 4 | const subtotal = items.reduce(function(acc, item) { |
| 5 | return acc + item.price * item.quantity; |
| 6 | }, 0); |
| 7 | |
| 8 | // Apply discount if one was provided |
| 9 | const afterDiscount = discountPercentage |
| 10 | ? subtotal - subtotal * (discountPercentage / 100) |
| 11 | : subtotal; |
| 12 | |
| 13 | // Calculate final total with tax |
| 14 | const tax = afterDiscount * taxRate; |
| 15 | const total = afterDiscount + tax; |
| 16 | |
| 17 | return Math.round(total * 100) / 100; |
| 18 | } |
| 19 | |
| 20 | // After Terser minification (1.1 KB — 66% reduction) |
| 21 | function calculateTotal(e,t,r){let n=e.reduce((e,t)=>e+t.price*t.quantity,0);let o=r?n-n*(r/100):n;let i=o*t;let a=o+i;return Math.round(a*100)/100} |
| 22 | |
| 23 | // After Terser + gzip (412 bytes — 87% reduction) |
| 24 | // (Cannot show binary gzip output) |
| 25 | |
| 26 | // SWC minification — 10-20x faster than Terser |
| 27 | // SWC is written in Rust and can minify a 100KB file in ~5ms vs ~80ms for Terser |
| Minifier | Language | Speed | Reduction | Use Case |
|---|---|---|---|---|
| Terser | JavaScript | Moderate | ~65% | Webpack production builds |
| SWC | Rust | Fast | ~60% | Next.js, SWC-based tooling |
| esbuild | Go | Fastest | ~55% | Vite, esbuild CLI |
| UglifyJS | JavaScript | Slow | ~60% | Legacy projects (not maintained) |
info
Compression reduces the transfer size of assets over the network. While minification reduces source code size, compression further reduces the already-minified output. This is typically handled by your web server (Nginx, CDN), but can also be pre-compressed during the build.
| 1 | // Compression algorithms comparison |
| 2 | // Original file: bundle.js (300 KB minified) |
| 3 | |
| 4 | // gzip compression (level 6 — default) |
| 5 | // bundle.js.gz: 85 KB (72% reduction) |
| 6 | // Compression time: ~50ms |
| 7 | // Decompression time: ~5ms (hardware-accelerated) |
| 8 | |
| 9 | // Brotli compression (quality level 11) |
| 10 | // bundle.js.br: 72 KB (76% reduction) |
| 11 | // Compression time: ~200ms (slower to compress) |
| 12 | // Decompression time: ~3ms (very fast to decompress) |
| 13 | |
| 14 | // Brotli vs gzip summary: |
| 15 | // - Brotli: 4-6% better compression ratio, slower to compress |
| 16 | // - gzip: universally supported, faster to compress |
| 17 | // - Modern browsers all support both |
| 18 | // - Pre-compression: compress once at build time, serve pre-compressed files |
| 19 | |
| 20 | // Build-time pre-compression with Webpack |
| 21 | const CompressionPlugin = require("compression-webpack-plugin"); |
| 22 | |
| 23 | module.exports = { |
| 24 | plugins: [ |
| 25 | new CompressionPlugin({ |
| 26 | algorithm: "gzip", |
| 27 | test: /\.(js|css|html|svg)$/, |
| 28 | threshold: 10240, // Only compress files > 10 KB |
| 29 | minRatio: 0.8, // Only keep if compression ratio is < 0.8 |
| 30 | }), |
| 31 | // Brotli pre-compression |
| 32 | new CompressionPlugin({ |
| 33 | algorithm: "brotliCompress", |
| 34 | test: /\.(js|css|html|svg)$/, |
| 35 | threshold: 10240, |
| 36 | minRatio: 0.8, |
| 37 | filename: "[path][base].br", |
| 38 | }), |
| 39 | ], |
| 40 | }; |
warning
Tree-shaking is the elimination of unused exports from the final bundle. It relies on the static structure of ES modules — since imports and exports are known at parse time (not runtime), the bundler can determine exactly which exports are used and which are dead.
| 1 | // Tree-shaking example — exports that matter |
| 2 | // src/utils/math.ts |
| 3 | export function add(a, b) { return a + b; } |
| 4 | export function subtract(a, b) { return a - b; } |
| 5 | export function multiply(a, b) { return a * b; } |
| 6 | export function divide(a, b) { return a / b; } |
| 7 | |
| 8 | // Breaking tree-shaking — side effects! |
| 9 | export const config = (() => { |
| 10 | // This immediately-invoked expression is a side effect |
| 11 | window.__MY_APP_CONFIG = { debug: false }; |
| 12 | return { debug: false }; |
| 13 | })(); // ← Side effect! This code will ALWAYS be included |
| 14 | |
| 15 | // Tree-shaking-friendly pattern |
| 16 | export function getConfig() { |
| 17 | return { debug: false }; |
| 18 | } // ← No side effect! Only included if called |
| 19 | |
| 20 | // Import pattern that enables tree-shaking |
| 21 | // GOOD — named imports enable dead code elimination |
| 22 | import { add, multiply } from "./utils/math"; |
| 23 | |
| 24 | // BAD — namespace import breaks tree-shaking |
| 25 | import * as mathUtils from "./utils/math"; |
| 26 | // Everything from mathUtils is now in scope — can't tree-shake |
| 27 | |
| 28 | // WORST — default import of barrel |
| 29 | import Utils from "./utils"; // ← May include everything! |
info
Content-based hashing adds a hash of the file's contents to its filename. When the content changes, the hash changes, producing a new filename. This enables aggressive caching — browsers can cache assets forever because a changed file gets a new URL, invalidating the cache naturally.
| 1 | // Webpack — content hash configuration |
| 2 | module.exports = { |
| 3 | output: { |
| 4 | filename: "[name].[contenthash:8].js", |
| 5 | chunkFilename: "[name].[contenthash:8].chunk.js", |
| 6 | assetModuleFilename: "assets/[hash][ext][query]", |
| 7 | }, |
| 8 | optimization: { |
| 9 | // Extract runtime chunk — only changes when the build system changes |
| 10 | runtimeChunk: "single", |
| 11 | // Split vendor chunks for stable caching |
| 12 | splitChunks: { |
| 13 | cacheGroups: { |
| 14 | vendor: { |
| 15 | test: /[\/]node_modules[\/]/, |
| 16 | name: "vendors", |
| 17 | chunks: "all", |
| 18 | // Only create vendor chunk if > 20 KB |
| 19 | minSize: 20480, |
| 20 | // Stable vendor hash — doesn't change when app code changes |
| 21 | priority: 10, |
| 22 | }, |
| 23 | // Group common dependencies |
| 24 | common: { |
| 25 | minChunks: 2, |
| 26 | priority: 5, |
| 27 | reuseExistingChunk: true, |
| 28 | }, |
| 29 | }, |
| 30 | }, |
| 31 | }, |
| 32 | }; |
| 33 | |
| 34 | // Example output filenames after hashing: |
| 35 | // main.4a7b2f9e.js — app code |
| 36 | // vendors.c3d8e1f5.js — node_modules (stable hash) |
| 37 | // runtime.8f2a1b3c.js — Webpack runtime (rarely changes) |
| 38 | // styles.f6e4d2c1.css — extracted CSS |
| 39 | // logo.9b1a3c5f.svg — static asset |
warning
You cannot optimize what you cannot measure. Bundle analysis tools visualize your output to identify large dependencies, duplicate code, and opportunities for optimization.
| 1 | // webpack-bundle-analyzer — visualize bundle composition |
| 2 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); |
| 3 | |
| 4 | module.exports = { |
| 5 | plugins: [ |
| 6 | new BundleAnalyzerPlugin({ |
| 7 | analyzerMode: "static", // Generate HTML report |
| 8 | reportFilename: "report.html", |
| 9 | openAnalyzer: false, // Don't auto-open |
| 10 | generateStatsFile: true, // Generate stats.json |
| 11 | statsFilename: "stats.json", |
| 12 | // Exclude source maps from analysis |
| 13 | defaultSizes: "gzip", |
| 14 | }), |
| 15 | ], |
| 16 | }; |
| 17 | |
| 18 | // CLI: Generate and analyze bundle stats |
| 19 | // npm run build -- --profile --json > stats.json |
| 20 | // Then upload to https://statoscope.tech or use webpack-bundle-analyzer |
| 21 | |
| 22 | // Statoscope — advanced bundle analysis |
| 23 | // npm install --save-dev @statoscope/webpack-plugin |
| 24 | const StatoscopePlugin = require("@statoscope/webpack-plugin").default; |
| 25 | |
| 26 | module.exports = { |
| 27 | plugins: [ |
| 28 | new StatoscopePlugin({ |
| 29 | saveReportTo: "statoscope-report.html", |
| 30 | saveStatsTo: "statoscope-stats.json", |
| 31 | watchMode: false, |
| 32 | // Set validation rules |
| 33 | rules: [ |
| 34 | { query: "size > 100 KB", severity: "warn" }, |
| 35 | { query: "duplicates > 0", severity: "error" }, |
| 36 | ], |
| 37 | }), |
| 38 | ], |
| 39 | }; |
info
A performance budget sets hard limits on bundle size, load time, or other metrics. When the budget is exceeded, the build fails. This prevents performance regressions from reaching production.
| 1 | // Webpack performance budget configuration |
| 2 | module.exports = { |
| 3 | performance: { |
| 4 | maxEntrypointSize: 250000, |
| 5 | maxAssetSize: 100000, |
| 6 | hints: "error", |
| 7 | assetFilter: function (assetFilename) { |
| 8 | return !assetFilename.endsWith(".map"); |
| 9 | }, |
| 10 | }, |
| 11 | }; |
warning
Code splitting is the most effective optimization for application bundles. By splitting your code into smaller chunks loaded on demand, you reduce the initial bundle size and improve time-to-interactive.
- Route-based splitting — Split at page boundaries. Each route loads its own chunk.
- Component-based splitting — Lazy-load heavy components (charts, editors, maps) that are below the fold.
- Library-based splitting — Extract large libraries (moment.js, lodash) into separate vendor chunks.
- On-interaction splitting — Load code only when the user interacts (modal click, form focus).
- Viewport-based splitting — Load components when they enter the viewport using IntersectionObserver.
| 1 | // Route-based splitting (Next.js — automatic) |
| 2 | // Next.js automatically code-splits by page — no config needed |
| 3 | // pages/index.tsx → chunk for / |
| 4 | // pages/dashboard.tsx → chunk for /dashboard |
| 5 | // pages/settings.tsx → chunk for /settings |
| 6 | |
| 7 | // Component-based splitting (React.lazy) |
| 8 | import React, { Suspense } from "react"; |
| 9 | |
| 10 | const HeavyChart = React.lazy(() => import("./HeavyChart")); |
| 11 | const PDFViewer = React.lazy(() => import("./PDFViewer")); |
| 12 | |
| 13 | function Dashboard() { |
| 14 | const [showChart, setShowChart] = useState(false); |
| 15 | |
| 16 | return ( |
| 17 | <div> |
| 18 | <button onClick={() => setShowChart(true)}> |
| 19 | Show Chart (loads on click) |
| 20 | </button> |
| 21 | {showChart && ( |
| 22 | <Suspense fallback={<div>Loading chart...</div>}> |
| 23 | <HeavyChart /> |
| 24 | </Suspense> |
| 25 | )} |
| 26 | </div> |
| 27 | ); |
| 28 | } |
| 29 | |
| 30 | // Library-level splitting (Webpack splitChunks) |
| 31 | // Vendors chunk extracted automatically — stable cache key |
Images and fonts are typically the largest assets on a page. Optimizing them during the build can dramatically reduce load times without compromising quality.
| 1 | // Image optimization plugins |
| 2 | // imagemin-webpack-plugin — lossless and lossy compression |
| 3 | const ImageminPlugin = require("imagemin-webpack-plugin").default; |
| 4 | const imageminMozjpeg = require("imagemin-mozjpeg"); |
| 5 | const imageminPngquant = require("imagemin-pngquant"); |
| 6 | const imageminSvgo = require("imagemin-svgo"); |
| 7 | |
| 8 | module.exports = { |
| 9 | plugins: [ |
| 10 | new ImageminPlugin({ |
| 11 | test: /\.(jpe?g|png|gif|svg)$/i, |
| 12 | plugins: [ |
| 13 | imageminMozjpeg({ quality: 80, progressive: true }), |
| 14 | imageminPngquant({ quality: [0.6, 0.8] }), |
| 15 | imageminSvgo({ plugins: [{ removeViewBox: false }] }), |
| 16 | ], |
| 17 | }), |
| 18 | ], |
| 19 | }; |
| 20 | |
| 21 | // Font subsetting — remove unused glyphs |
| 22 | // Only include characters your app actually uses |
| 23 | // Tools: glyphhanger, fonttools, subfont |
| 24 | |
| 25 | // Webpack font loader with hash |
| 26 | module.exports = { |
| 27 | module: { |
| 28 | rules: [ |
| 29 | { |
| 30 | test: /\.(woff2?|eot|ttf|otf)$/i, |
| 31 | type: "asset/resource", |
| 32 | generator: { |
| 33 | filename: "fonts/[name].[hash:8][ext]", |
| 34 | }, |
| 35 | }, |
| 36 | ], |
| 37 | }, |
| 38 | }; |
info