The missed chances: What minifiers leave behind


Know thy enemy and know thyself

I want to figure out how minifiers work and I'm happy to take you with me.

I have long procrastinated on this task. It really bothers me that Remynification works. The intuition is that repeated CSS minification should not actually do much good. Once you compressed something it is a foolish idea to compress it again.

But... It works. I need to find out why. I want to be able to do better.

I plan to test the CSS minifiers on various small files, to see what optimizations they perform and which optimizations are missed. I don't expect we will have all the answers in short order, but bit by bit it will get done.

The journey will not be easy. I have prepared my armor and my thick skin. I have my gladius at my side. My trusty travel cup is filled with delicious diet pop, sweetened only with the finest sucralose and acesulfame-potassium that the kingdom has to offer. I still have no idea what I'm doing.

I'm sure nothing can go wrong.

Starting simply

My family has two mottoes. The one that we use in public is a famous zen koan: "To conquer the world, start with nothing". Accordingly, we will start with nothing as well. Let's minify an empty file. I have just the thing!

$ csso /dev/null | wc -c
1
$ crass /dev/null | wc -c
1
$ cssnano /dev/null | wc -c
0
$ cleancss /dev/null | wc -c
ERROR: Ignoring local @import of "../../../../../dev/null" as resource is missing.
0

As you can see, csso and crass manage to bloat the input, by infinity percent. Also, cleancss tells me that my /dev/null does not exist, which no one else had any issues with. Interesting. We are off to a great start here, aren't we?

Now, to be fair, I am evaluating the command-line tools here. The minifiers themselves might be just fine. I still don't appreciate a byte being generated out of nothing though. A whole byte! Oh, the humanity!

Ahem. Moving on.

The byte in question is whitespace. To compensate for this in future samples, I will manually equalize the whitespace between the minifiers, to make it easier to see what is going on.

Whitespace removal

Right... Whitespace. There are some optimizations that any minifier ought to be able to perform without any issues. Minification should remove all useless white space from the CSS file. Not quite as easy as running sed over the CSS file. For instance, you need some separation in selectors...

p em {color: red}

... where reducing p and em to pem is not a good idea.

Let's see how we do on tabs, spaces and newlines. I could show you the input file, but it wouldn't be very exciting. Let's just go with the comparison of what happens.

$ cat whitespace.css | wc -c
14
$ cleancss whitespace.css | wc -c
0
$ crass whitespace.css | wc -c
1
$ cssnano whitespace.css | wc -c
0
$ csso whitespace.css | wc -c
1

All minifiers do just fine, the one last lousy byte notwithstanding.

Empty selector removal

Selectors that change nothing should be removed. You aren't likely to handwrite code like this, but maybe some sloppy process you used generated that kind of horribleness. Either way, it needs to go.

$ cat empty.css
p{}
$ cleancss empty.css
$ crass empty.css --optimize
$ cssnano empty.css
$ csso empty.css

Everyone did well here. No complaints, but then again I'd be amazed if any minifier got this wrong.

De-facto empty selector removal

Finally, even if I dump a bunch of semicolons into a CSS file, I'd think those could go as well.

$ cat broken.css
p{;;;;;;;;;;;;}
$ cleancss broken.css
$ crass broken.css --optimize
$ cssnano broken.css
$ csso broken.css

Poof! Gone like the wind. I have no complaints.

Let's move on.

Come, color my world!

Ah, gone is the era of the calmer, black and white, silent Internet. The Internet is now filled with sound, color and fury now. Being a complete bookworm, I'm not sure if this is a good thing, but it is here to stay. Since it isn't going anywhere, might as well make the use of color in CSS as compact as it could be.

Colors are complicated. There are multiple forms in which you can specify colors with some forms calling for special treatment of some colors. Simple search and replaces won't work very well here, you need to actually do some work.

Name to hexadecimal reduction

First, let's take a look at my favourite color, rebeccapurple.

$ cat color.css
p {background-color:rebeccapurple}

Here's what the minifiers did with this.

$ cleancss color.css -O2 all:on
p{background-color:#663399}

"rebeccapurple" is longer than the hexadecimal description of this color. It makes sense to use the shorter representation. Let's see what the other minifiers do.

$ crass color.css --optimize --O1
p{background-color:#639}
$ cssnano color.css
p{background-color:#639}
$ csso color.css
p{background-color:#639}

Yup. Fun fact, #XYZ is shorthand for #XXYYZZ. You just double each letter to get the actual color. Neat.

A second iteration of cleancss produces the shorter form as well.

Let's try a more tricky color.

Hexadecimal reduction

$ cat color2.css
p{font-color: #ff0000}

We all remember the trick, right? We can write this as #f00! The minifiers should know this, right?

$ cssnano color2.css
p{font-color:#ff0000}

It appears that cssnano does not know the trick. In fact, I have never seen it use the four-character form for colors, aside from when converting from a color written out in English. My testing was limited to just what I'm showing here, but I would have thought it would have come into play at least once.

A bit of a correction, coming from Ben Briggs. cssnano does not optimize colors when in combination with font-anything, which is what I was doing here. My mistake, the actual property is actually called "color" and cssnano works there just fine. The reason why this isn't modified is that you really don't want the word black to be replaced indiscriminately in other settings, the font of "arial black" coming to Ben's mind. Thanks, Ben, much love! This one's for the regression tests. Other minifiers had no issues with changing this for a nonexistent property. I left this mistake of mine in, and will go with the corrected code from this point on.

Let's see how the rest did. They might know the trick!

$ cleancss color2.css
p{color:red}
$ crass color2.css --optimize --O1
p{color:red}
$ csso color2.css
p{color:red}

The minifiers can do even better than the four-character notation. Writing "red" is actually one character shorter than using even the shorthand. One character saved isn't much, but it all adds up. Tan is a similarly compact color and the same rules apply. None of the minifiers special-cased just red, tan works fine wherever red did. Yup, sometimes using the English description of a color is a good thing.

Color rounding

I was curious to see if any of the minifiers would be eager to fudge things a little bit and "round" a color that is almost at the point where it could be represented by a shorter form.

$ cat color3.css
p{color:#663398}

This is almost #639. Almost, but not quite. Minifiers, what do you say to this?

$ cleancss color3.css -O2 all:on
p{color:#663398}
$ crass color3.css --optimize --O1
p{color:#663398}
$ cssnano color3.css
p{color:#663398}
$ csso color3.css
p{color:#663398}

No. Not one minifier chooses to break colors. I'd say this is a good thing.

Reducing HSL colors

You can specify colors in other forms than just RGB. Here's one of the many ways to speak about the color black:

$ cat color4.css
p{color:hsl(13,37%,0%)}

The expectation is that the minifiers will reduce this to a nice #000. Let's see what actually happens.

$ cssnano color4.css
p{color:#000}
$ cleancss color4.css -O2 all:on
p{color:#000}
$ crass color4.css --optimize --O1
p{color:#000}
$ csso color4.css
p{color:#000}

Everyone did fine.

Transparency simplification

Transparencies are fun! Sometimes, however, they just get in the way. Here we have a color that is actually completely transparent.

$ cat color5.css
p{color:hsla(180,50%,50%,0)}

How would you handle it? Can this be made any shorter?

$ cleancss -O2 all:on color5.css
p{color:hsla(180,50%,50%,0)}
$ cssnano color5.css
p{color:rgba(64,191,191,0)}
$ csso color5.css
p{color:rgba(64,191,191,0)}
$ crass color5.css --optimize --O1
p{color:transparent}

No points gets cleancss, having done nothing. Something interesting happens with csso and cssnano, where they reduced the hsla form to a shorter rgba form. crass was the only minifier to write the color as "transparent". Clever beast.

This does make me wonder why isn't there a # form for transparencies. I could get behind #ff0000ff or #f00f. Seems like an odd inconsistency not to have it, and it would be shorter. Maybe there is one and I just don't know about it?

One more update from Ben: #foof is in CSS4. Yay!

Transparency dropping

We had colors that are completely transparent, how about colors that are completely opaque?

$ cat color5b.css
p{color:hsla(180,50%,50%,1)}
$ cleancss color5b.css -O2 all:on
p{color:hsla(180,50%,50%,1)}
$ crass color5b.css --optimize --O1
p{color:#40bfbf}
$ cssnano color5b.css
p{color:#40bfbf}
$ csso color5b.css
p{color:#40bfbf}

Everyone except cleancss managed to discover that there is no actual need for transparency anything here.

Keep in mind, it isn't that often that you will see colors that are fully opaque or fully transparent. I'm just trying to be thorough.

Not-colors

So... How about being a bit overeager? How about something that looks like a color, but isn't?

$ cat color7.css
h2:before{content:"black"}
$ csso color7.css
h2:before{content:"black"}
$ cleancss -O2 all:on color7.css
h2:before{content:"black"}
$ crass color7.css --optimize --O1
h2:before{content:"black"}

So far so good... But then...

$ cssnano color7.css
h2:before{content:"#000"}

Whoops. Time to file a bug report?

Closing thoughts

I'll stop here, the article is getting long and I have to mark exams of my much beloved undergraduate students. Oh, academia and your hiring of adjuncts! If only I had a TA, but I guess that was just a little bit too expensive for you.

I have ten more different simplifications already processed and to be written up for later. Maybe next week? We'll see.

To conquer the world, start with nothing.

I find it very amusing to be doing this. I'm probably wrong somewhere along the lines, but I'm loving this. Speaking of which, corrections, digressions and suggestions welcome, my email is in the footer.

Infinite bloat is much more fun than it ought to be, even if it is by one byte and probably not even introduced by the minifier itself.

I'm for hire! If anyone has got any cool stuff to do, preferably writing, researching or teaching, get in touch.

This is going to take a while. There is a lot that can be done to CSS... I wish I could start doing what I want to be doing, but I would like to understand what the current minifiers are doing.

Oh, hey, look. A donation button. A miserable, sad, lonely creature that no one cares about. But wait, you can help! Become friends with the button. Get to know the button. Feed the button. Feed it well, would you kindly?

Donate

Past: The race for the smallest CSS files continues