Minifying and Uglifying JavaScript Code for Production
Learn to minify and uglify JavaScript for production. Covers Terser configuration, mangling strategies, compression options, source map handling, dead code elimination, build pipeline integration, and measuring bundle size reduction.
Minification removes whitespace, comments, and shortens variable names to reduce JavaScript bundle size. Uglification goes further with advanced compression and mangling. This guide covers Terser (the modern standard) and build pipeline integration.
For tree shaking and dead code removal, see Removing Dead Code with JS Tree Shaking Guide.
Minification vs Uglification
// ORIGINAL SOURCE (1,247 bytes)
class ShoppingCart {
constructor() {
this.items = [];
this.discount = 0;
}
/**
* Add an item to the shopping cart
* @param {Object} item - The item to add
* @param {number} quantity - How many to add
*/
addItem(item, quantity = 1) {
const existingItem = this.items.find(
(cartItem) => cartItem.id === item.id
);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ ...item, quantity });
}
return this;
}
// Calculate the total price with discount
getTotal() {
const subtotal = this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return subtotal * (1 - this.discount / 100);
}
}
// MINIFIED (whitespace + comments removed, ~450 bytes)
// class ShoppingCart{constructor(){this.items=[];this.discount=0}addItem(item,quantity=1){const existingItem=this.items.find(cartItem=>cartItem.id===item.id);if(existingItem){existingItem.quantity+=quantity}else{this.items.push({...item,quantity})}return this}getTotal(){const subtotal=this.items.reduce((sum,item)=>sum+item.price*item.quantity,0);return subtotal*(1-this.discount/100)}}
// UGLIFIED/MANGLED (~280 bytes)
// class ShoppingCart{constructor(){this.items=[];this.discount=0}addItem(t,n=1){const i=this.items.find(e=>e.id===t.id);return i?i.quantity+=n:this.items.push({...t,quantity:n}),this}getTotal(){return this.items.reduce((t,n)=>t+n.price*n.quantity,0)*(1-this.discount/100)}}| Technique | What It Does | Size Reduction | Risk |
|---|---|---|---|
| Whitespace removal | Removes spaces, newlines, tabs | 20-30% | None |
| Comment removal | Strips all comments | 5-15% | None |
| Variable mangling | Renames locals to short names | 10-20% | Low (if configured right) |
| Property mangling | Renames object properties | 5-15% | High (can break code) |
| Dead code elimination | Removes unreachable code | 5-30% | Low |
| Compression passes | Simplifies expressions | 5-10% | Low |
Terser Configuration
// terser.config.js - comprehensive configuration
const { minify } = require("terser");
async function minifyCode(code, filename) {
const result = await minify(code, {
// Parsing options
parse: {
ecma: 2020,
},
// Compression options
compress: {
ecma: 2020,
passes: 2, // Multiple compression passes
drop_console: true, // Remove console.* calls
drop_debugger: true, // Remove debugger statements
pure_funcs: [ // Functions known to have no side effects
"console.log",
"console.info",
"console.debug",
],
pure_getters: true, // Assume property access has no side effects
unsafe_math: true, // Optimize math expressions
unsafe_methods: true,
dead_code: true, // Remove unreachable code
unused: true, // Drop unused variables/functions
conditionals: true, // Optimize if-else into ternary
evaluate: true, // Evaluate constant expressions
booleans: true, // Optimize boolean expressions
loops: true, // Optimize loops
join_vars: true, // Join consecutive var statements
collapse_vars: true, // Collapse single-use variables
hoist_funs: true, // Hoist function declarations
toplevel: false, // Do not mangle top-level names
global_defs: {
DEBUG: false,
PRODUCTION: true,
},
},
// Mangling options
mangle: {
toplevel: false,
safari10: true, // Work around Safari 10 bugs
properties: false, // Do NOT mangle property names (dangerous)
reserved: ["$", "exports", "require"], // Never mangle these
},
// Output formatting
output: {
ecma: 2020,
comments: false, // Remove all comments
beautify: false,
ascii_only: true, // Escape non-ASCII characters
wrap_iife: true, // Wrap IIFEs in parentheses
},
// Source map
sourceMap: {
filename: `${filename}.map`,
url: `${filename}.map`,
},
});
return result;
}
// Usage
const code = fs.readFileSync("dist/bundle.js", "utf8");
const minified = await minifyCode(code, "bundle.min.js");
fs.writeFileSync("dist/bundle.min.js", minified.code);
fs.writeFileSync("dist/bundle.min.js.map", minified.map);
console.log(`Original: ${(code.length / 1024).toFixed(1)}KB`);
console.log(`Minified: ${(minified.code.length / 1024).toFixed(1)}KB`);
console.log(`Reduction: ${((1 - minified.code.length / code.length) * 100).toFixed(1)}%`);Compression Techniques Deep Dive
// Terser compression transformations explained
// 1. Conditional simplification
// Before:
if (condition) {
return true;
} else {
return false;
}
// After: return !!condition; (or just: return condition)
// 2. Boolean optimization
// Before: x === true -> After: x
// Before: x === false -> After: !x
// Before: !!(x && y) -> After: x && y (when used as boolean)
// 3. Dead code after return
function example() {
return 42;
// Everything below is removed
console.log("never reached");
doCleanup();
}
// 4. Constant folding
// Before: const x = 2 * 3 + 1;
// After: const x = 7;
// 5. Collapse single-use variables
// Before:
const temp = getValue();
doSomething(temp);
// After:
doSomething(getValue());
// 6. Sequence expressions
// Before:
a = 1;
b = 2;
c = 3;
return c;
// After: return a = 1, b = 2, c = 3;
// 7. Template literal to string (when simpler)
// Before: `Hello ${name}`
// After: "Hello " + name (sometimes smaller)Property Mangling (Advanced)
// Property mangling is DANGEROUS but can save significant size
// Only use with a well-defined public API
const { minify } = require("terser");
async function minifyWithPropertyMangling(code) {
return minify(code, {
mangle: {
properties: {
// Only mangle properties starting with underscore
regex: /^_/,
// Never mangle these properties
reserved: [
"constructor",
"prototype",
"toString",
"valueOf",
"hasOwnProperty",
],
},
},
});
}
// Source code convention for safe property mangling:
class EventEmitter {
constructor() {
this._listeners = new Map(); // Will be mangled
this._maxListeners = 10; // Will be mangled
}
// Public API (not mangled)
on(event, handler) {
if (!this._listeners.has(event)) {
this._listeners.set(event, []);
}
this._listeners.get(event).push(handler);
}
emit(event, ...args) {
const handlers = this._listeners.get(event) || [];
handlers.forEach((h) => h(...args));
}
// Private helper (will be mangled)
_validateListener(handler) {
if (typeof handler !== "function") {
throw new TypeError("Listener must be a function");
}
}
}Source Maps for Production
// Source maps let you debug minified code in production
// Option 1: External source maps (recommended)
// bundle.min.js -> points to bundle.min.js.map
// Upload .map to error tracking (Sentry, etc.)
// Do NOT serve .map to public
// Terser source map configuration
const result = await minify(code, {
sourceMap: {
content: inputSourceMap, // Chain with previous source maps
filename: "bundle.min.js",
url: "bundle.min.js.map",
// Or use inline: includeSources: true
},
});
// Option 2: Upload maps to Sentry/error tracking
async function uploadSourceMaps(version) {
const maps = fs.readdirSync("dist").filter((f) => f.endsWith(".map"));
for (const mapFile of maps) {
const jsFile = mapFile.replace(".map", "");
await fetch("https://sentry.io/api/0/projects/org/proj/releases/files/", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENTRY_TOKEN}`,
},
body: JSON.stringify({
name: `~/${jsFile}`,
file: fs.readFileSync(`dist/${mapFile}`, "utf8"),
}),
});
}
}
// Option 3: Nginx/server config to restrict .map access
// location ~* \.map$ {
// allow 10.0.0.0/8; # Internal IPs only
// deny all;
// }Build Pipeline Integration
// Webpack with Terser
// webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
mode: "production",
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: { drop_console: true, passes: 2 },
mangle: { safari10: true },
output: { comments: false },
},
extractComments: false,
}),
],
},
};
// Vite (uses esbuild for dev, Terser or esbuild for prod)
// vite.config.js
export default {
build: {
minify: "terser", // or "esbuild" (faster but less optimal)
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
};
// esbuild (fastest minifier)
// esbuild src/index.js --bundle --minify --sourcemap --outfile=dist/bundle.js
// SWC minifier (fast Rust-based alternative)
// npx swc src/index.js -o dist/bundle.js --minifyMeasuring Minification Results
const fs = require("fs");
const { gzipSync, brotliCompressSync } = require("zlib");
function measureFile(filePath) {
const content = fs.readFileSync(filePath);
const raw = content.length;
const gzipped = gzipSync(content).length;
const brotli = brotliCompressSync(content).length;
return {
file: filePath.split("/").pop(),
rawKB: (raw / 1024).toFixed(1),
gzipKB: (gzipped / 1024).toFixed(1),
brotliKB: (brotli / 1024).toFixed(1),
gzipRatio: ((1 - gzipped / raw) * 100).toFixed(1) + "%",
brotliRatio: ((1 - brotli / raw) * 100).toFixed(1) + "%",
};
}
function compareFiles(original, minified) {
const origStats = measureFile(original);
const minStats = measureFile(minified);
return {
original: origStats,
minified: minStats,
reduction: {
raw: ((1 - parseFloat(minStats.rawKB) / parseFloat(origStats.rawKB)) * 100).toFixed(1) + "%",
gzip: ((1 - parseFloat(minStats.gzipKB) / parseFloat(origStats.gzipKB)) * 100).toFixed(1) + "%",
brotli: ((1 - parseFloat(minStats.brotliKB) / parseFloat(origStats.brotliKB)) * 100).toFixed(1) + "%",
},
};
}
// Usage
const comparison = compareFiles("dist/bundle.js", "dist/bundle.min.js");
console.table([comparison.original, comparison.minified]);
console.log("Reduction:", comparison.reduction);| Minifier | Speed | Output Size | Language | Used By |
|---|---|---|---|---|
| Terser | Moderate | Smallest | JavaScript | Webpack, Vite (optional) |
| esbuild | Fastest | Slightly larger | Go | Vite (default), standalone |
| SWC | Very fast | Comparable to Terser | Rust | Next.js, standalone |
| UglifyJS | Slow (legacy) | Good | JavaScript | Legacy projects |
Rune AI
Key Insights
- Terser is the standard production minifier: It provides the smallest output through multi-pass compression, dead code elimination, and variable mangling
- Always generate source maps for production: Upload them to error tracking services for debuggable stack traces without exposing them to the public
- Compression (Gzip/Brotli) is complementary to minification: Minification reduces raw size, compression reduces transfer size; always use both together
- Avoid property mangling unless you have strict conventions: Renaming object properties can break dynamic access patterns, external API interactions, and serialization
- Multiple compression passes (passes: 2+) improve output: Each pass discovers new optimization opportunities created by the previous pass, especially for large bundles
Frequently Asked Questions
Does minification affect JavaScript performance at runtime?
Should I use esbuild or Terser for production?
Is property mangling safe to use?
How do I debug minified code in production?
Should I gzip or Brotli compress in addition to minifying?
Conclusion
Minification with Terser provides 50-70% raw size reduction through whitespace removal, comment stripping, variable mangling, and expression compression. Combined with Gzip or Brotli compression, total transfer size drops by 85-95%. Integrate minification into your build pipeline and use source maps for production debugging. For dead code removal, see Removing Dead Code with JS Tree Shaking Guide. For bundler configuration, see Webpack vs Vite vs Rollup: JS Bundler Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.