Summary of the contribution
I came up with Remynification, a new way to minify CSS. This technique beats every competing technique that I am aware of, on every CSS file that I threw it its direction. To my knowledge this has not been done before. The savings over the previous state of the art vary, but you can expect to save at least a few bytes. I am making a proof-of-concept Remynifier available for free and open source. There is a catch: Remynification takes forever and may break your CSS, even though this isn't really my fault.
Setting out on a great journey
Funny. I never thought I'd basically own the leaderboards for CSS minification. And yet... Here I am. The person who used to foam at the mouth at everything to do with the dread HTTP is now contributing a minifier. Well, a meta-minifier.
I often ask myself: How did I find myself here? You see, I blame my new boss. Things got hostile and I wasn't having fun anymore. I loved teaching at the university as long as the management let me love it. With the wintry chill approaching I had to start working on creating an escape pod from my eternal adjunct job. I decided to write textbooks and make them available for free. Maybe ad-supported, maybe take donations. I'm not sure yet, but I want education to be accessible. It won't earn much, but every little bit helps if you are stuck as an adjunct. And, well... I do love teaching.
Much like a typical academic, I suffer from the Not Invented Here Syndrome. I created my own content, static site generator, set of tools to automatically build and publish the pages, basically everything. And, of course CSS. I wrote that from scratch and I'm pretty proud of that, even though it kinda sucks.
Thing is, I really like efficiency. I have spent a long time doing combinatorial optimization. I care about small file sizes and fast render times. After a short while I started asking myself if I can make my website any smaller, to make it be impressive at launch. I don't really do JavaScript and HTML minification is pretty much a solved problem as far as I can see. Zopfli was fun for a post-processing step. I also wanted to get the smallest CSS possible, but I was not really happy with the minifiers available.
The basic idea of a minifier
A minifier is a funny program. In comes CSS. Out comes CSS that should mean the same thing as the CSS that went in, but smaller. Free saved bytes, yay!
I tried a few, each did things a bit differently. Yes, I got some decent file size reductions. There's a tiny problem though... I'm obsessive. I don't want just a good result. I want the very best result possible. I ended up feeling unsatisfied. No minifier was doing everything that I wanted it to do.
I started to play around. One thing that I can do is to run every minifier possible and to pick the smallest result to save as my minified CSS. I can't lose! Why choose when you don't have to?
Let's see how this works on bootstrap.css. First, let's see what is the starting size of this file.
$ wc -c bootstrap.css
146079 bootstrap.css
Ok. Let's see how the minifiers do.
$ cat bootstrap.css | csso | wc -c
116399
$ cat bootstrap.css | cssnano | wc -c
116104
$ crass bootstrap.css --optimize --O1 | wc -c
111022
$ cat bootstrap.css | cleancss -O2 all:on | wc -c
110225
Well, that worked. I could just keep the best one and call it a day.
But...
The oldest trick in the book
A minifier has CSS as both the input and as the output. How about we just... feed the output back in as input, in case something got missed the first time around?
$ cat bootstrap.css | csso | csso | wc -c
116482
$ cat bootstrap.css | cssnano | cssnano | wc -c
116100
$ crass bootstrap.css --optimize --O1 > tmp.css
$ crass tmp.css --optimize | wc -c
111022
$ cat bootstrap.css | cleancss -O2 all:on | cleancss -O2 all:on | wc -c
109901
(The code boxes are scrollable, if the whole thing does not fit on your screen.)
Yeah, didn't think that would work, did you?
We can re-minify the files! Hmm... Let's spell it Remynifying. Yes, with a capital R. Because I'm vain.
csso managed to bloat the input a bit. cssnano won 4 more bytes. crass didn't change anything. cleancss managed to shave off the most.
Progress!
I really don't want to choose!
Even then, we are picking one minifier and just running it twice.
Why not... Run all of them in a nice chain?
$ crass bootstrap.css --optimize --O1 | csso | cssnano | cleancss -O2 all:on | wc -c
103259
Holy heck. Sweet Gods, we are saving quite a bit at this point. We are now done. We are running all of the minifiers. There cannot be anything left anymore. Can't do any better. ...Or can we?
Yes, we can!
The four minifiers arranged in a pipeline are all well and good, but they can still miss some optimizations. Maybe we should have started with csso. Or maybe crass should finish off? We don't know!
So... How about we just go through all of the possibilities? We can allow for duplicate appearances of minifiers as well, just to cover our bases. We need to take a look at all the minifier chains of length 1, to find the best single minifier. We then need to check all the minifier chains of length 2, to find the best combination of two minifiers. This basically amounts to a tree search over all of the possible minifier chains.
Tree searching is pretty simple. You have a queue of items that need to be processed. You take out an item from the queue and run some operation on it. Once done, you might put some new items into the queue, for future processing.
I have created a program that does just this. The whole thing is a very simple loop. I take out a work item from my queue. Each work item is a combination of a CSS file and a minifier that I'm supposed to run on that file. I run this minifier on that file, getting a new result. I then insert into the queue commands which will process the new result. Along the way I keep track of which result is the best, so that I can choose the smallest file.
I put in some smarts to make it go a bit faster in the case where you want a result (relatively) quickly.
$ julia remynifier.jl bootstrap.css
++R+D+D+DR+D+DR@DRRDDRDDDDDDDDDDDDDD
Best size: 103083
Minifier sequence: bootstrap.css>csso>crass>cleancss -O2 all:on>cleancss -O2 all:on>cleancss -O2 all:on>cssnano>cleancss -O2 all:on
Minifier sequence: bootstrap.css>csso>crass>cleancss -O2 all:on>cleancss -O2 all:on>cleancss -O2 all:on>cssnano>cleancss -O2 all:on>cssnano
This search took fifty-eight seconds on my machine. The way to obtain this file turns out to be seven steps long. csso, crass, three runs of cleancss, cssnano and one more run of cleancss. The final run of cssnano does not reduce the file size further, but provides an equivalently-sized file that is different than the previous one. This isn't an optimal result obtainable through Remynification, but is of decent quality given the time spent.
The best result that I ever manage to obtain was:
Best size: 102822
Minifier sequence: bootstrap.css>cssnano>csso>cleancss -O2 all:on>crass>cleancss -O2 all:on>cleancss -O2 all:on
Minifier sequence: bootstrap.css>cssnano>csso>cleancss -O2 all:on>crass>cleancss -O2 all:on>cleancss -O2 all:on>cssnano
It took 25 extra minutes to save this 261 bytes. Worth it? You decide.
Benchmark results
Let's see how this approach compares to the competition. I'm pulling in the results from here. I'm also letting the Remynifier run for hours.
File | Original size | Previous best | Remynified |
---|---|---|---|
960.css | 9989 | 5713 | 5709 |
animate.css | 72258 | 51476 | 32357 |
blueprint.css | 17422 | 10488 | 10071 |
bootstrap.css | 146079 | 116112 | 102822 |
font-awesome.css | 34778 | 28385 | 27591 |
foundation.css | 105994 | 73148 | 61574 |
gumby.css | 167695 | 143698 | 110820 |
inuit.css | 53132 | 17058 | 16074 |
normalize.css | 7278 | 2056 | 2040 |
oocss.css | 40151 | 13288 | 12539 |
pure.css | 31318 | 15954 | 14455 |
reset.css | 1092 | 747 | 745 |
(The table can be scrolled around if it does not fit on your screen. Just drag with your finger or use the scrollbar.)
The total size reduction is about 17% across the entirety of the benchmark set, always comparing against the best minifier for a given CSS file.
I have seen faster glaciers, you know...
To Remynify we have to run minification a ton of times. This has two major problems with it.
First problem: As I said, it takes a really long time to run through all the possibilities. What previously took a couple of seconds now can stretch into an hour or two.
For the speed issue we can prune our search. If we have previously seen a given result of minification, then I don't need to queue up it for minification again, it would just be a repeat. Sooner or later the minifiers will start producing the same result over and over, so this will cut down on the number of items we chew through.
We are not guaranteed that we eventually exhaust the search queue. There could exist a combination of minifiers that, when run repeatedly, will endlessly bloat the CSS. Because the CSS keeps growing, there is an infinite number of files possible and... Yikes! The solution to that is limiting my searching to a fixed depth. I found the depth of five to be very good, if maybe on the "has a really long runtime" side. Myself, I stick with depth of one, which is also the default. This does not mean that only short chains will be found. This means that the Remynifier will be pretty pessimistic about results which don't look promising at first glance. Higher search depths will infuse the software with more optimism and cheer, making it keep on trying even if things look pretty grim.
Even with a high depth, good results are obtained fairly quickly. Items that either match or improve the current smallest result get inserted into the front of the search queue. The search is depth-first for the children of the items that were an improvement and breadth-first otherwise. This is just a heuristic that tries to get good results early in the search.
The depth is overridden for results which are an improvement. From my experience doing combinatorial optimization, improvements tend to come in groups. I ignore the depth limit for the children of the items which are improvements. This is why, when optimizing bootstrap.css with the depth limit of one, I still obtained a minifier chain of length seven. Each time an improvement was found, it was permitted to queue up further work despite this work being deeper than the limit.
I could use something like a GRASP or do some hill climbing to maybe be a bit more efficient. Thing is, this works well enough for me. Even the longest minification took under an hour on my laptop. My own CSS gets minified in just a couple of minutes. Good enough.
Even if you initially decide that you'd like to run the software to a big depth, you don't need to have this program run to completion. The output file will be continuously updated with every improvement found. Considering the runtime can go into hours, this is probably a good thing, no?
Look, ma! No CSS!
Here is the second issue: If one of the minifiers damages your CSS then the final output will be garbage.
Well, that one is a bit more tricky. CSS minification does not have to be a lossless operation, even though we often think of it as such. I would not be surprised if at least one minifier rounds things at some point. I don't usually do CSS design, I'm more of a teacher and combinatorial optimizer (can you tell?). I don't run all this heavy CSS. I know that it worked mostly fine on my own, tiny, CSS and I'm happy with it.
In my use case, everything was working great, with one exception. Chrome could not cope with the unit of 'q' being used. Firefox didn't seem to mind, everything worked out of the box there. I manually changed the units to something Chrome could work with and everything was fine. (My guess is that I was misusing some bit of CSS.) I can imagine a minifier could do a lot more damage with a huge CSS file where manual fixing is not an option.
If a minifier performs some kind of lossy CSS compression then it will cause the CSS to degenerate into a mess with enough repetitions. Of course, if a minifier in the group you are using causes such issues, you can always adjust its options or remove it from consideration altogether, relying on the others.
This does lead me to an interesting question. Are the current CSS minifiers correct? How would we measure/verify it? The benchmarks don't comment on it, but it would be good if we could find out. Some of the results I got were dubious, animate.css and gumby.css most notably. I have a hard time believing that there was this much inefficiency in the original files. This feels lossy.
If lossy CSS minification is a thing that we are willing to put up with, I would like to present to you the optimal minifier, which will quickly make every CSS file the smallest size possible, at the cost of some detail being lost.
The code!
Yes! It is time!
The CSS Remynifier is available right here: Download.
This is just a proof of concept. It will run with an elegance of a slug full of lettuce. I'm sure you could make it go ten times as fast with just small improvements. Maybe if I weren't running minifiers from within shell commands from within Julia...
You need to install the Julia programming language to be able to use the Remynifier. You also need the command-line versions of the four minifiers that I'm using.
Yeah, typical academic code, uncommon programming language included. Don't even get me started on the quality. I'm sorry. It isn't that I don't know how to code. I had about three hours of spare time and didn't want to spend forever on writing things properly. A solution that completely runs in JS would be tons better, if someone feels like giving this a stab. I loathe JavaScript too much to ever learn the thing.
I handle crashing minifiers, as well as ones that loop forever. Yes, it happens. Don't people fuzz test the damn things? Sheesh! QA, anyone?
Well... To be fair, I didn't do much QA either. Oh, well! ;)
Closing thoughts
I wondered if this approach would work on JS and on HTML.
Seems like UglifyJS is just too good. I wasn't able to beat it. Then again, I didn't want to spend too much time on this. I'm happy to mess around with those ideas, if someone pays me. Speaking of which...
Feed the Remy
My turn to ask for something.
I'm just an adjunct at a second-rate university. I don't have much money. My MSc in computer science is going to waste. I'd like to fix that.
I'm mainly looking for academic work, especially doing research to further the cause of conservation or similar work in geography. I'm also happy to work with you on other projects that may need someone with computer skills. Heck, I would have loved to have written this one up as a proper paper. Too bad that no one cared about it. I suspect that at least one of my ideas is publishable, in machine learning over decision trees. I have ideas for more CSS stuff too. Get in touch if you'd like to work together.
I would also like to be hired to keep working on this, on similar techniques or on other of my pet code projects that I think could benefit others. I don't take pleasure in releasing things in a half-baked way and moving on. I'd like to do a good job. I wish I could. I have a couple more ideas and if paid I will gladly spend the time on this.
I also take donations. Email me for that, you can see my contact details in the page footer.
And if all that happened was that you got a laugh out of this insanely stupid idea, well... That's really the best kind of support you can give me. Thanks! :)