The race for the smallest CSS files continues


The journey marches on

A week or two ago, I have released the CSS Remynifier onto the world. I have received many comments and I have been tinkering with CSS on and off to take the comments into account.

So far we only have one bug in csso fixed out of that. Not a bad start, but there are so many more things to investigate.

Let's go on and get some more data about how compression works together with minification.

It will be compressed, Remy!

Quite a few people have, rightly, mentioned that CSS almost never gets sent down the wire uncompressed. This is true.

I will run a large comparison here. We will have the inputs minified using various minifiers and compressed using various compression methods. Let's see how they compare and how they match up.

First, meet the contestants:

In the compression corner we have gzip -9, the gold standard for compression that is used on the web. You don't go for gzip because you want the smallest files possible. You go for gzip because you want to pack things up quickly and need the files to be unpacked everywhere.

Also on team compression, zopfli. Zopfli produces files in the same format as gzip, but compressed better. The good news is that every piece of software that can decode a file encoded with gzip can also decode files done with zopfli.

Finally, brotli, the problem child. No one loves brotli. Servers still don't support it out of the box as much as they should. Browser support is a bit under 60%. Brotli, however, offers much better compression rates and does not run glacially, though well-trained racing turtles do give it a bit of a challenge.

In the other corner, we have the four minifiers: csso, crass, cssnano and cleancss. All minifiers seem decently capable of doing their jobs. We don't have any of this "remove spaces" stuff here. Each minifier can perform transformations that actually reduce the size of the CSS that is being minified.

I'm also going to run the Remynifier using a combination of the four minifiers. Because I can and it amuses me, that's why. I won't make it the focus. Let's go.

The results are in!

First of all, let's take a look at the results from the compression run.

(Remember, the tables are draggable/scrollable if they don't completely fit on your screen.)

File size comparison (bytes) -- compression
File nameOriginalgzip -9zopfli
960.css9,9891,7111,604
animate.css72,2584,3714,079
blueprint.css17,4224,9884,658
bootstrap.css146,07920,90419,482
font-awesome.css34,7786,8586,338
foundation.css105,99415,84114,714
gumby.css167,69522,35120,148
inuit.css53,13213,81413,116
normalize.css7,2782,2042,125
oocss.css40,1517,8387,428
pure.css31,3187,7957,391
reset.css1,092625603

Yes, using zopfli saves bytes over using gzip. Let's throw in brotli too. I'm running it at the quality level of 11 and with window setting of 24. This is the maximum setting available and it is still quite quick.

File size comparison (bytes) -- compression
File namezopflibrotli
960.css1,6041,194
animate.css4,0793,536
blueprint.css4,6583,728
bootstrap.css19,48217,051
font-awesome.css6,3385,515
foundation.css14,71413,157
gumby.css20,14817,317
inuit.css13,11611,433
normalize.css2,1251,783
oocss.css7,4286,544
pure.css7,3916,511
reset.css603470

Brotli is a big win, even over zopfli. Runtime not being my goal, I did not time it, but it did run noticeably faster as well.

Apparently using a better algorithm/file format is a better idea than trying to make the old one behave. Reverse compatibility is both a blessing and a pain that we have to live with.

I won't waste too much of my time in experimenting with silly things, but compressing bootstrap.css with the paq method, as seen in lrzip, gives me a file size of 16364. We are nowhere near the level of insanity that is required to use paq for CSS, so I'll end this here. Not like there's browser support for that anyway.

Next up...

The minifiers step up!

Instead of relying on the data from a table I found online, this time I ran all the minifiers myself.

File size comparison (bytes) -- minifiers
File namecssocrasscssnanocleancss
960.css5,7695,7145,7725,768
animate.css55,18536,13551,47655,630
blueprint.css10,70410,50310,72710,406
bootstrap.css116,399111,022116,104110,225
font-awesome.css28,53327,72728,38528,102
foundation.css73,42472,13175,55669,075
gumby.css150,260122,155144,266139,150
inuit.css17,83717,33918,02516,499
normalize.css2,0572,1092,1572,082
oocss.css14,48313,78014,37512,952
pure.css17,06514,91816,22115,603
reset.css748746773773

Well, that's a nice table, but kinda hard to put it into context. Let me help. I'm going to pick the best minifier for a given file and I'm going to compare it against brotli.

Compression vs minification (bytes)
File nameOriginal sizeOnly minifiedOnly compressed
960.css9,9895,7141,194
animate.css72,25836,1353,536
blueprint.css17,42210,4063,728
bootstrap.css146,079110,22517,051
font-awesome.css34,77827,7275,515
foundation.css105,99469,07513,157
gumby.css167,695122,15517,317
inuit.css53,13216,49911,433
normalize.css7,2782,0571,783
oocss.css40,15112,9526,544
pure.css31,31814,9186,511
reset.css1,092746470
total687,186428,61088,239

When minifying, by using the best minifier for a given file, we saved 38% over the entire corpus. By compressing with brotli we saved 87%.

This is quite interesting. Compression ran much faster and produced much better results than minification. If you are going to use only one technique to make your CSS smaller, make it be some kind of compression. There is also something to be said about the ability of minifiers to make undesirable changes to the CSS. There is probably some degree of safety in using a non-optimizing minifier together with compression.

Speaking of which!

How about we work together?

Nothing limits us from working together. We can combine minification and compression. I take the best result of each minifier and feed it into brotli. The last column of the table will tell you which of the minifiers produced the file that squeezed down the most.

We are all in this together!
File nameBest minifiedCompressedBothMinifier
960.css5,7141,194703cssnano
animate.css36,1353,5362,896cssnano
blueprint.css10,4063,7282,131cleancss
bootstrap.css110,22517,05115,132crass
font-awesome.css27,7275,5155,073crass
foundation.css69,07513,1579,954crass
gumby.css122,15517,31714,239crass
inuit.css16,49911,4333,255crass
normalize.css2,0571,783701crass/csso
oocss.css12,9526,5442,791crass
pure.css14,9186,5112,968crass
reset.css746470330crass
total428,61088,23960,173

Yup, brotli and minification are a great combination. I was a bit worried and I'm glad to be shown wrong. You see, this is just a rumour, but someone I know has supposedly seen some cases where gzip could not cope very well with the results of minification. Apparently a minified file would compress down to a larger size than an unminified one. Brotli has no such issues. It, at maximum settings, eats all that CSS up for breakfast and asks for a second helping. Yummy.

There is an interesting behaviour happening here as well. The smallest CSS file after minification does not always correspond to the smallest file when compressed. For example, 960.css was at the smallest size when minified with crass. However, the smallest .br resulted from initially minifying with cssnano and then applying brotli. I think that a big benefit to minification would be to try to consider the effects of compression on the output files. Something to give more thought to, I reckon.

I wonder if crass optimizes for compressed size. It did really well here. I saw some reordering of selectors and values, to what looks like alphabetic order, and this would help compression. Congratulations, team crass, you are the best at giving CSS squeezy hugs.

Enter the Remynifier

So... how does this work for everyone's favourite thought experiment in CSS butchery? The usual caveat of this possibly wrecking your CSS still applies (though I have not seen many issues on real-life CSS that I'm using), but let's do it for the sake of playing around. What is the absolutely lowest file size that we can get from our input CSS files, under the assumption that Remynifying is safe enough for our use case?

Since this is just a funny experiment and I'm busy enough with my day job, I will do this on just one file. I hear a lot about this bootstrap thing, so let's go with that.

bootstrap.css sizes
bytesSavings over previous
Original146,079
Minified110,22524.5%
Remynified102,8226.7%
Compressed17,05183.5%
Compressed+Minified15,13211.2%
Compressed+Remynified15,060:(

Well, it did improve things, but not exactly in a way I'd call very impressive. At this point we are playing CSS size-reducing drag racing. Not saying it isn't fun, but the practicality just isn't there, unless you are pretty desperate. (Which happens. I have been contacted by some pretty desperate people, with requests to Remynify their CSS for them. Yes, you saw right. People: Plural.)

But I think we can do a bit better.

The Remynifier needs a change. Previously I had it only deal with the CSS files, aiming to get the smallest CSS file. Now, however, we are in need of the smallest CSS file after compression. This is a bit harder. While we hope that a smaller input CSS file will produce a smaller compressed file, we have no guarantees.

Good thing about the search procedure is that it can cope with a bit of disconnect between the metric we are actually optimizing (compressed size) and the outputs of our minifiers (not compressed). Simple: I apply brotli to the output of the minifiers when checking for an improvement over the previous best answer. I still keep the CSS file for the purposes of feeding it to a next minifier in the pipeline, but the best file is decided based on the results of the brotli compression. I'm still hoping that the chain of CSS files which are smaller under compression will, when followed, lead me to a good result quickly.

Of course, because our search is not as guided as before, we need to use a bigger search depth.

Here's what I, much later, got!

Best size: 14941
Minifier sequence: bootstrap.css>cssnano>crass>csso>csso>cssnano

Yay! I have a further 119 bytes saved!

I'm not sure this is worth it, considering that the saved bytes might have come from your CSS being mangled. But yes, we can still, in theory, save those bytes. If damage wasn't an issue, I'd just go for it.

The search, sadly, devolved into being completely brute-force. It is hard to predict the compressed size from the minified size, which makes it hard to quickly discover a good chain of minifiers. It took quite a long time, to say the least.

Closing thoughts

I'm still a bumbling fool who barely knows what they are doing. (Corrections welcome! Email them in!)

Brotli is awesome. Use it.

I'll be releasing a new version of the Remynifier in a few days, one that can handle targeting a compressed size better.

crass is awesome under brotli. I'm probably going to be using it.

Minification is nowhere near as important as compression. The two play well together, at least in case of brotli.

I should write up something to detect minifiers breaking the CSS. Easier said than done.

I also should write up something about what the minifiers change and look for missed optimizations. I'm aiming for next week.

Making a minifier that does really well under compression seems like a neat task, if maybe a solved one.

I need to lose weight. Too many Swiss bready things. I thought it would help, but the more I eat, the worse it gets! Why doesn't it work the same way as with compression?

And if you guys be willing to give me a tiny bit of change so I could break even on this whole subproject, that would be lovely! Thanks.

Donate

Past: The lesser known CPUs: dsPIC33F