Every minifier advertises the same number: your stylesheet got 30% smaller. That number is measured on the raw file, and the raw file is not what anybody downloads. Once gzip is in the path, removing all the indentation from a stylesheet saves about four percent. Removing the comments can save fifty.
The Same Files, Measured Both Ways
Three real stylesheets, each run through two passes: one that removes only whitespace, and one that also removes comments. Everything is measured as it would be served, with gzip at level 9.
The pattern holds across all three. Whitespace is nearly free once it is compressed, because gzip's whole job is spotting repeated sequences and a stylesheet's indentation is the most repetitive thing in the file. Comments are not repetitive. They are unique prose, they compress about as badly as text ever does, and they are the only part of a stylesheet with no effect on rendering at all.
What Each Pass Is Worth After Compression
Gzipped sizes. The percentage is the saving against the gzipped original, not the raw file.
| Stylesheet | Whitespace only | Whitespace and comments |
|---|---|---|
| This site's globals.css, 8.9 kB | −4.4% | −28.2% |
| Tailwind theme.css, 19.5 kB | −4.7% | −5.0% |
| Tailwind preflight.css, 8.4 kB | −3.3% | −58.2% |
Why preflight.css Loses Sixty Percent and theme.css Loses Five
Tailwind's preflight.css is a reset, and a reset is mostly argument. Almost every rule in it carries a comment explaining which browser bug it exists for and what breaks if you remove it. Strip those and well over half the compressed file goes with them.
theme.css in the same package is a list of custom properties: hundreds of lines of --color-red-500: oklch(…). There is barely a comment in it, and the lines are so similar to each other that gzip already encodes them at close to nothing. Minifying it is very nearly a waste of a build step.
That is the useful shape of the rule. The compressed saving from minification tracks how much of your file is prose, not how much of it is formatting. A stylesheet written by hand with a paragraph above every section has a lot to gain. A generated one has almost none, which is why the CSS your build tool emits is already minified and re-minifying it changes nothing.
Three spaces that are not decoration
calc(100% - 20px) needs the spaces around the minus; without them the -20px is read as a single negative number and the expression is invalid. a :hover with a space is a descendant selector and a:hover without one is not, so a minifier that strips before every colon silently retargets your rule. And @media screen and (min-width: 640px) cannot become and(min-width:640px), because and( tokenizes as a function. All three are ordinary CSS, and all three are how a naive find-and-replace minifier corrupts a file.
Why This Cannot Be Done With Regular Expressions
The instinct is to reach for a couple of replacements: kill /\*.*?\*/, collapse \s+, done. It falls over on the first real stylesheet.
A comment marker inside a string is not a comment: content: "/*" is a valid declaration. A semicolon inside a url() is not the end of a declaration, and every data URI has one, right there in url(data:image/svg+xml;base64,…). A brace inside a content string is not the end of a block. Each of these needs the file to be read as tokens rather than as text, which is what CSS Syntax Module Level 3 specifies: comments, strings and url tokens are consumed whole before anything else looks at them.
There is one more trap that only shows up once the tokenizer is right. A comment is not nothing; it separates tokens. width:100px/**/; is the old IE hack and removing the comment is free, but in foo/*x*/bar the comment is the only thing keeping two identifiers apart, and deleting it outright fuses them into foobar. The safe move is to replace a removed comment with a space and let the ordinary spacing rules decide whether that space can then go.
Minify a stylesheet, or read one back
Comments and whitespace out, structure untouched, with the before and after size.
The Optimizations Worth Refusing
Past whitespace and comments, minifiers start rewriting meaning. Each of these is a real feature in some tool, and each has a failure mode that is invisible until production.
Removing duplicate declarations
`display:-webkit-box` followed by `display:flex` is not a duplicate, it is a fallback: the old browser takes the first and ignores the second, the new one takes the last. Deduplicating by property name deletes support for whichever browser you were propping up.
Merging rules with the same selector
Two blocks with the same selector can be separated by a third rule that overrides one of their properties. Pull them together and the cascade order changes underneath you.
Collapsing longhands into a shorthand
`margin-top` and `margin-bottom` are not `margin`. The shorthand also resets `margin-left` and `margin-right` to zero, which throws away whatever they inherited or were set to elsewhere.
Dropping units from zero
`0px` and `0` are interchangeable for a length, and they are not for everything: `<time>` and `<angle>` reject a bare zero, so `transition: all 0s` must keep its unit or the declaration is dropped. Flex is its own trap, where `flex-basis: 0` and `0px` differ in older implementations.
Reordering anything at all
The cascade is defined by document order for rules of equal specificity. Any reordering is a bet that nothing in the file relies on it, and the payoff is a handful of bytes that gzip would probably have found anyway.
What to Actually Do
Serve CSS compressed. This is the whole game and it is a server setting, not a build step: gzip takes a typical stylesheet to about a quarter of its size, and brotli beats it by another ten to fifteen percent. If compression is off, no amount of minification will make up the difference.
Then minify, mostly for the comments. Keep the /*! ones, which is the convention every major minifier honours for license headers that have to survive. If your CSS ships under MIT or Apache-2.0 and carries an attribution notice, that notice is a licensing obligation and not a comment you get to delete.
And stop measuring the raw file. The number that matters is what leaves the server, which you can see in the network panel under content-length with content-encoding: gzip beside it. A build step that reports 31% smaller while the wire sees 4% is not lying, it is just answering a question nobody asked.
Frequently Asked Questions
Related Tools
Keep Reading
Why HSL Says Yellow and Blue Are Equally Light
Two colors, both hsl 50% lightness, eight to one apart in contrast. Where the sRGB gamma curve hides the difference, and how OKLCH puts it back.
HTML Entities: Five Characters, and Why Escaping Depends on Context
Only five characters truly need escaping, and doing it is not enough on its own. Where each context needs its own rule, and how double encoding announces itself.
JSON Errors Explained: Trailing Commas, NaN and Other Rejections
Why valid-looking JSON fails to parse, how to read a parser's error position, and the number precision bug that silently corrupts large IDs.
Base64 Is Not Encryption: What It Does and Why It Exists
Three bytes become four characters, which is where the 33 percent size increase and the trailing equals signs come from. And why it hides nothing.