What Minification Does to CSS, JavaScript and HTML
Drafted with AI assistance. Every command and code example was run and its output checked before publication. How guides are made
Minification rewrites CSS, JavaScript or HTML into a smaller file that behaves the same: it removes comments and whitespace, shortens values and, for JavaScript, renames local variables and deletes code that can never run. It is not a replacement for HTTP compression. On this site's own JavaScript file, Terser cut 66,085 bytes to 36,667, but gzip alone took the original to 16,878 bytes, and minifying first then compressing brought it to 11,981. Minify and compress for the smallest download, and use source maps so you can still debug the result.
What does a minifier actually remove?
A minifier parses the file and then prints it back out as compactly as possible. How far it can go depends on the language. The MDN glossary entry describes the basic idea; the table shows which techniques apply where.
| Technique | CSS | JavaScript | HTML |
|---|---|---|---|
| Remove comments | Yes (licence comments /*! … */ are often kept) | Yes (Terser keeps /*!, @license and @preserve by default) | Yes (conditional comments are usually kept) |
| Collapse whitespace | Yes, except where it is significant | Yes; newlines only where needed | Only where it cannot render; one space must often remain |
| Shorten values | #ff0000 → red, 0px → 0 | 0.2 → .2, true → !0 | Remove optional quotes and tags (aggressive tools only) |
| Mangle names | No (class names are public) | Local variables and parameters | No |
| Remove dead code | Duplicate or overridden rules (some tools) | Unreachable branches, unused locals, constant folding | No |
JavaScript: compress and mangle
Terser, the minifier used by the JavaScript Formatter, has two main stages: compress rewrites and removes code, and mangle renames local identifiers. Given this function:
// Add VAT to a price
function priceWithVat(price) {
const DEBUG = false;
const rate = 0.2;
if (DEBUG) {
console.log('price', price);
}
return price * (1 + rate);
}
console.log(priceWithVat(10));
Terser 5 with { compress: true, mangle: true } outputs:
function priceWithVat(t){return 1.2*t}console.log(priceWithVat(10));
The comment is gone, the if (DEBUG) block was removed because the condition is always false, 1 + rate was folded to 1.2 and the parameter became t. The function name was kept: in a classic script, a top-level function is a global that other scripts might call, so Terser does not rename it unless you pass toplevel: true or minify an ES module. With compress and mangle both turned off, the same input loses only its comment and whitespace, apart from small printing changes such as 0.2 becoming .2.
CSS: whitespace, values and rule merging
CSS has no local variables to rename, so the gains come from whitespace, comments and shorter values. For this stylesheet:
/* Buttons */
.btn {
margin: 0px 0px;
color: #ff0000;
}
.btn :hover {
color: rgb(0, 0, 255);
}
Lightning CSS 1.33 with minify: true outputs .btn{color:red;margin:0}.btn :hover{color:#00f}: it shortened the colours and the margin, and reordered the two declarations, which is safe here because they set different properties. The CSS Formatter's Minify button gives .btn{margin:0px 0px;color:#ff0000}.btn :hover{color:rgb(0,0,255)}; it removes comments and whitespace but leaves values as written.
HTML: careful whitespace handling
HTML minifiers have the least room to work, because in normal text a run of whitespace is displayed as one space. Removing it completely would glue words together. Aggressive tools such as html-minifier-terser also drop optional closing tags and attribute quotes and minify inline <style> and <script> contents, but each of those options has trade-offs.
How much does minification save compared with gzip and Brotli?
Less than most people expect once the server compresses responses, but it is not nothing. We measured three of this site's own files on Node.js 20: assets/common.css, assets/common.js and the HTML of the YAML pitfalls guide. Compression used Node's built-in zlib with gzip at level 9 and Brotli at quality 11 (the maximum for each). Sizes are in bytes.
| File and minifier | Raw | gzip -9 | Brotli 11 |
|---|---|---|---|
| common.js, original | 66,085 | 16,878 | 14,642 |
| common.js, Terser 5.51 (compress + mangle) | 36,667 | 11,981 | 10,544 |
| common.js, Terser, whitespace and comments only | 47,798 | 14,165 | 12,524 |
| common.css, original | 27,472 | 6,214 | 5,432 |
| common.css, this site's CSS Minify | 25,145 | 5,526 | 4,881 |
| common.css, Lightning CSS 1.33 | 24,852 | 5,490 | 4,858 |
| yaml-pitfalls HTML, original | 26,588 | 7,850 | 6,534 |
| yaml-pitfalls HTML, this site's HTML Minify | 24,982 | 7,691 | 6,397 |
| yaml-pitfalls HTML, html-minifier-terser 7.2 | 24,778 | 7,652 | 6,377 |
What these numbers show:
- Compression does most of the work. gzip alone shrank the JavaScript by 74.5% and the CSS by 77.4%. Minification alone saved 44.5% and 9.5%.
- The two stack. Minified and Brotli-compressed, the JavaScript is 10,544 bytes, 84% smaller than the original and 28% smaller than Brotli on the unminified file. Mangling and dead-code removal matter: whitespace-only minification left about 2,000 more bytes after Brotli.
- Already-tidy files gain little. The stylesheet is written compactly, so even a full CSS optimiser saved only 724 bytes after gzip. The HTML page is mostly prose; after gzip, minifying it saved 159–198 bytes, about 2%.
Your files will differ, so measure your own. This script prints the same three sizes for any file:
// size.js — usage: node size.js file.js
const fs = require('fs');
const zlib = require('zlib');
const buf = fs.readFileSync(process.argv[2]);
const gz = zlib.gzipSync(buf, { level: 9 }).length;
const br = zlib.brotliCompressSync(buf, {
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 },
}).length;
console.log({ raw: buf.length, gzip: gz, brotli: br });
gzip is specified in RFC 1952 and Brotli in RFC 7932. Servers often compress on the fly at lower levels than these, so real-world compressed sizes are usually a little larger.
What are source maps and how do I generate one?
A source map is a JSON file that maps each position in the minified output back to a line and column in the original source, so browser developer tools can show readable code, original names and correct line numbers in stack traces. The format is standardised as ECMA-426; see also MDN's overview. The minified file points to its map with a final comment, or the server sends a SourceMap HTTP header.
npx terser app.js --compress --mangle \
--source-map "url='app.min.js.map'" --output app.min.js
With Terser 5.51, for a four-line add() function this writes app.min.js ending in //# sourceMappingURL=app.min.js.map and a map whose names array lists the original identifiers (add, first, second…). Anyone who can download the map can read your original code, so decide deliberately whether to publish maps or upload them only to your error-tracking service.
What can minification break?
A minifier that properly parses the language rarely breaks valid code. The problems come from code that relied on something the minifier legitimately changed, or from simple tools that use text substitution instead of a parser.
Automatic semicolon insertion in JavaScript
JavaScript inserts missing semicolons only in specific situations (ECMAScript, automatic semicolon insertion). A line starting with ( or [ continues the previous line:
const a = 1
const b = a
(function () { console.log(b) })()
This is already a bug before minification: a(function …) is a call. Terser parses it the same way and prints const a=1,b=1(function(){console.log(b)})();, which makes the mistake visible. The danger is a naive minifier that just deletes newlines from code that was relying on them; a parser-based tool like Terser does not do that. If you omit semicolons, start such lines with ;.
Whitespace between inline elements and in <pre>
In <strong>20%</strong> <em>today</em> the space between the elements is visible text. Removing it renders "20%today". Inside <pre>, <textarea> or any element styled with white-space: pre, every space and line break is content. A safe minifier keeps one space between inline elements and leaves <pre> alone; it cannot know about whitespace your own CSS makes significant. MDN explains how whitespace is handled.
a :hover is not a:hover
In a selector, a space is the descendant combinator. a :hover matches any hovered element inside a link; a:hover matches the hovered link. A minifier must keep that space, while in a declaration such as color : red the space before the colon can go. Both Lightning CSS and this site's CSS Minify keep a :hover intact.
SCSS // comments
// is not a comment in plain CSS. In SCSS mode, the site's CSS Minify removes it; with CSS selected, the input .n { a: b; // note followed by c: d; } on the next line becomes .n{a:b;// note c:d}, which breaks the c declaration, and Lightning CSS refuses to parse it. Compile SCSS with Sass first, or choose SCSS before minifying.
Comments next to !important, and licence comments
A comment between a value and !important, as in color: red /* keep */ !important, is valid; minifiers remove it and keep the flag (color:red!important). The more common surprise is licence text. Many licences require the copyright notice to stay with the code, and by convention a comment starting with /*! marks it as one to keep. Terser's default comments: "some" keeps /*! … */ and comments containing @license or @preserve, and Lightning CSS kept a /*! comment in our test. This site's CSS Minify keeps /*! comments too and removes all others.
How do this site's Minify buttons work?
All three run in your browser, and each takes a different approach:
- JavaScript Formatter: runs Terser 5 (loaded from jsDelivr) with
compress: true, mangle: trueand Terser's other defaults, so licence comments are kept and top-level names are not renamed. It does not produce a source map. Minify is disabled for TypeScript; compile to JavaScript first. - CSS Formatter: a tokeniser-based minifier written for the page. It removes all comments, collapses whitespace, drops spaces around
{ } ; ,and the last semicolon in a block, and leaves strings,url()values and escapes untouched. It does not shorten colours or merge rules. With SCSS selected it understands//comments and#{…}interpolation. If Sort rules is ticked, rules are sorted before minifying. - HTML Formatter: a conservative minifier. Runs of whitespace become one space, which is removed only next to document-level tags such as
<head>,<meta>and<body>. Comments are removed except conditional comments. The contents of<pre>,<textarea>,<script>and<style>are left exactly as written, so inline CSS and JavaScript are not minified.
For example, the HTML Minify button turns a comment, a paragraph with extra spaces and a <pre> block into <p> Save <strong>20%</strong> <em>today</em> </p> <pre> followed by the original, unchanged lines of the <pre> block. The spaces just inside <p> are kept on purpose: the tool cannot tell whether they would render.
Should I minify in my build or with an online tool?
For anything you deploy repeatedly, minify in the build. webpack in production mode and Vite's production build minify by default, esbuild does with --minify, and Rollup does through a plugin. A build step can produce source maps automatically and runs the same way every time, so there is no step to forget and no risk of deploying a stale hand-minified file. Keep the readable source in version control and treat minified files as build output.
An online minifier is useful for one-off jobs: a snippet for a CMS field, an inline script for an email template, a quick check of how much a file would shrink, or a third-party script you want to compare. To go the other way, the Beautify buttons on the same pages make minified code readable again, although mangled local names stay short. The Diff Checker helps compare two versions.
Checklist
- Enable gzip or Brotli on the server first; it saves more than minification on its own.
- Minify JavaScript with a parser-based tool (Terser, esbuild) and allow mangling and compression.
- Generate source maps and decide whether they are public.
- Keep
/*!licence comments where the licence requires them, and check your CSS minifier does too. - Compile SCSS, TypeScript and JSX before minifying.
- Do not rely on whitespace in HTML that a minifier could collapse, except in
<pre>. - Test the minified build, not just the source.
Frequently asked questions
Is minification still worth it if my server uses Brotli?
Usually yes for JavaScript. In our measurement, Terser plus Brotli produced a file 28% smaller than Brotli alone, because renaming and dead-code removal take out information that compression cannot. For CSS and HTML the extra saving was much smaller, a few hundred bytes per file.
Is minified code the same as obfuscated code?
No. Minification aims only to reduce size; a formatter can restore the layout, and only local names are lost. Obfuscation deliberately makes code hard to understand, and the result can be larger, not smaller.
Why did my minified JavaScript stop working?
The most common causes are code that depends on function or class names (which mangling can change), a missing semicolon before a line starting with ( or [, and syntax the minifier does not support, such as TypeScript or JSX that has not been compiled. Use a source map to find the failing line in the original code.
Should I commit minified files to version control?
Generally not. Commit the source and let the build produce minified files, so the two cannot drift apart. The usual exception is a library that publishes ready-to-use .min.js files for people who do not use a bundler.
Does minifying HTML help much?
On a prose-heavy page, not much: our test page lost about 6% raw and 2% after gzip. Large templates with deep indentation, many comments or inline scripts gain more, but compression and caching matter far more for HTML.