|$ curl https://forge-ai.dev/api/markdown?path=docs/build-tools/optimization
$cat docs/build-tools-—-build-optimization.md
updated Recently·14 min read·published

Build Tools — Build Optimization

Build ToolsIntermediate
Introduction

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

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.

minification-comparison.js
JavaScript
1// Before minification (3.2 KB)
2function 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)
21function 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
MinifierLanguageSpeedReductionUse Case
TerserJavaScriptModerate~65%Webpack production builds
SWCRustFast~60%Next.js, SWC-based tooling
esbuildGoFastest~55%Vite, esbuild CLI
UglifyJSJavaScriptSlow~60%Legacy projects (not maintained)

info

SWC minification is 10-20x faster than Terser with comparable output size. If your build pipeline supports SWC (Next.js does), prefer it over Terser for significant build time savings.
Compression

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.

compression-webpack.config.js
JavaScript
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
21const CompressionPlugin = require("compression-webpack-plugin");
22
23module.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

Do not compress small files. Compression has overhead. Files under 10 KB often compress poorly and may even increase in size. Set a threshold of at least 10 KB to avoid wasting build time on files that don't benefit from compression.
Tree-Shaking Deep Dive

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.

tree-shaking.ts
JavaScript
1// Tree-shaking example — exports that matter
2// src/utils/math.ts
3export function add(a, b) { return a + b; }
4export function subtract(a, b) { return a - b; }
5export function multiply(a, b) { return a * b; }
6export function divide(a, b) { return a / b; }
7
8// Breaking tree-shaking — side effects!
9export 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
16export 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
22import { add, multiply } from "./utils/math";
23
24// BAD — namespace import breaks tree-shaking
25import * as mathUtils from "./utils/math";
26// Everything from mathUtils is now in scope — can't tree-shake
27
28// WORST — default import of barrel
29import Utils from "./utils"; // ← May include everything!

info

Add ""sideEffects": false'} to your package.json to tell bundlers that your package has no side effects. This enables Webpack and Rollup to tree-shake more aggressively. If you have side-effect files (like CSS imports), list them explicitly: {"sideEffects": ["./styles.css"]}.
Asset Hashing & Long-Term Caching

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.

webpack.config.js
JavaScript
1// Webpack — content hash configuration
2module.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

Never use [hash] alone — use [contenthash] for JavaScript files. The plain [hash] changes on every build even if the file content hasn't changed, defeating long-term caching.
Bundle Analysis Tools

You cannot optimize what you cannot measure. Bundle analysis tools visualize your output to identify large dependencies, duplicate code, and opportunities for optimization.

bundle-analysis.js
JavaScript
1// webpack-bundle-analyzer — visualize bundle composition
2const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
3
4module.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
24const StatoscopePlugin = require("@statoscope/webpack-plugin").default;
25
26module.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

Run webpack-bundle-analyzer after every major dependency upgrade. A minor version bump of a library can silently add kilobytes to your bundle. Automate this check in CI using Statoscope's rule-based validation to catch regressions early.
Performance Budgets

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.

performance-budgets.js
JavaScript
1// Webpack performance budget configuration
2module.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

Set performance budgets based on user research, not arbitrary numbers. A 200 KB budget that your app consistently exceeds is worse than a 400 KB budget that is consistently met. Start with the 75th percentile of your current bundle size and tighten over time.
Code Splitting Strategies

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.
code-splitting-strategies.tsx
JavaScript
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)
8import React, { Suspense } from "react";
9
10const HeavyChart = React.lazy(() => import("./HeavyChart"));
11const PDFViewer = React.lazy(() => import("./PDFViewer"));
12
13function 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
Image & Font Optimization

Images and fonts are typically the largest assets on a page. Optimizing them during the build can dramatically reduce load times without compromising quality.

image-font-optimization.js
JavaScript
1// Image optimization plugins
2// imagemin-webpack-plugin — lossless and lossy compression
3const ImageminPlugin = require("imagemin-webpack-plugin").default;
4const imageminMozjpeg = require("imagemin-mozjpeg");
5const imageminPngquant = require("imagemin-pngquant");
6const imageminSvgo = require("imagemin-svgo");
7
8module.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
26module.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

For fonts, prefer WOFF2 format — it is supported in all modern browsers and offers 30-50% better compression than WOFF. Subset your fonts to include only the characters your application uses. A full CJK font can be 15 MB; a subset may be 50 KB.
$Blueprint — Engineering Documentation·Section ID: BT-OPT·Revision: 1.0