Missed chances 3: Optimizing bad CSS snippets


Searching within dreams for the best CSS minifier

I wake up screaming so often nowadays. Day after day, those disturbing dreams that push me to work on CSS and minifier-performed optimizations. The Elder Gods will awaken if I don't spend time on minifiers. Only my work on CSS keeps the world in the state we know it.

Well, I'm happy to save the world for another week! :)

Today I will be battering the minifiers with a few bits of valid CSS and with several bits of invalid CSS. Mostly invalid CSS, because I'm in the mood for some cheap laughs, since probably no one does handling of garbage CSS well.

It would be a good time to decide what a minifier ought to do when it encounters something absurd. I'm going to say that if there is a parse error, I think the minifier ought to do nothing, not even a partial output. In case of something clearly invalid, I'd expect a warning and either the problem bit of CSS to be removed from the output CSS or, better yet, left alone for the browser to deal with, just in case the minifier happened to be wrong.

I'm also operating with full CSS optimizations turned on, which increases the odds of stuff breaking.

As usual: I'm comparing the four minifiers against my expectations. I'm going to make comments based on my knowledge, but I am not an expert and so you shouldn't treat my words as an absolute truth. Maybe relative truth. Yeah, let's go with that.

Let's get started, we must save the world! :P

How about you calculate something for me?

CSS has this funny thing called calc(). The commonly cited use is combining values of different units. This is the time to dig up the idea of compile-time evaluation. If only we could compute the answer in the minifier... We'd be saving space AND we'd be saving computation time. What could possibly go wrong!

Let's find out!

Simple stuff and yet...

How about we just add two values in the same unit? As easy as it gets, right?

-- Original --
p { font-size: calc(15px+15px) }
-- Results --
Majority:
crass cssnano ----
p{font-size:30px}
Dissent group 1:
csso ----
p{font-size:calc(15px15px)}
Dissent group 2:
cleancss ----
p{font-size:calc(15px+15px)}

Welp. This is... not great. Cleancss leaves the whole thing alone, which is perfectly fine. Csso is very nonplussed about this. Crass and cssnano compute the value.

Things work better for csso when spaces are introduced.

-- Original --
p { font-size: calc(15px + 15px) }
-- Results --
Majority:
crass cssnano ----
p{font-size:30px}
Dissent group 1:
cleancss csso ----
p{font-size:calc(15px + 15px)}

This has to do with the fact that CSS actually needs the spaces. Roman has mentioned this on Twitter. Thanks to Roman there is now a fix to csso that should prevent the plus signs from being eaten.

Four squares a day

Multiplying is gonna be fun. Of course, I'm not going to do just a simple multiplication, I'm gonna be a weirdo and try to compute something 2D to fit a 1D value.

-- Original --
p { width: calc(15px*15px) }
-- Results --
Majority:
cleancss crass csso ----
p{width:calc(15px*15px)}
Dissent group 1:
cssnano ----
p{width:225px}

Hmm... Well, I'm not sure about what I'd do here. I'm going to hold with the majority opinion, I think. Cssnano picked a unit and multiplied the number. I don't think this is how units work, but then again I have serious doubts that the CSS I fed to the minifiers is valid.

Setting it right

Let's help out a bit. Two different units and good spacing. Still totally a bad idea to actually do the multiplication, but it should be more obvious now.

-- Original --
p { width: calc(10cm * 15px)}
-- Results --
Majority:
crass csso ----
p{width:calc(10cm*15px)}
Dissent group 1:
cleancss cssnano ----
p{width:calc(10cm * 15px)}

Two minifiers cleaned up the spaces, two didn't. None multiplied. Good. Not that this is valid CSS, but still...

Leet haxen yo!

I haz a hax.

-- Original --
p { width: calc(1337px / 0); }
-- Results --
Majority:
csso ----
p{width:calc(1337px/0)}
Dissent group 1:
cssnano ----
p{width:Infinitypx}
Dissent group 2:
crass ----
Dissent group 3:
cleancss ----
p{width:calc(1337px / 0)}

Yes. Much hax indeed. Sweet and so leet, yet not obsolete. I can totally divide by zero. Crass does not actually crash, it just produces a blank output, presumably having eliminated the division as something anathema. Cssnano seems to leak some JS, maybe? Csso gets rid of spaces and cleancss doesn't.

I'd like to buy an indulgence!

So... How about we break things a bit? I'm curious about what can be done with calc()... Trigonometry, maybe? Not legal CSS as far as I can tell, but not like that has ever stopped me.

-- Original --
a { width: calc(sin(90deg)); height: calc(cos(90deg)); }
-- Results --
Majority:
cleancss csso ----
a{width:calc(sin(90deg));height:calc(cos(90deg))}
Dissent group 1:
cssnano ----
a{width:1deg;height:0deg}
Dissent group 2:
crass ----
a{height:cos(90deg);width:sin(90deg)}

The bait is set and the trap is sprung.

Two minifiers don't touch anything. Cssnano has an issue with units, but not like sin() is legal within calc() as far as I could see. Hmm... More JS leakage? And crass just throws out the calc(), while keeping the insides. Interesting.

Actually decent CSS for once!

Can calc() work with colors?

-- Original --
html { background-color: rgb( calc(100+ 25) , calc(200 - 100), calc(0+2+3+4)) }
-- Results --
Majority:
crass cssnano ----
html{background-color:#7d6409}
Dissent group 1:
csso ----
html{background-color:rgb(calc(100+ 25),calc(200 - 100),calc(0234))}
Dissent group 2:
cleancss ----
html{background-color:rgb(calc(100+ 25) ,calc(200 - 100),calc(0+2+3+4))}

Yes, it can. Half of the time. Csso continues to be voracious for those delicious, delicious plus signs. Ah, yummy plus signs. I eat at least a dozen a day myself.

Once more, this invalid CSS should be handled better in the latest csso.

Are you stringing me along?

How about within strings? Strings should never, ever be touched.

-- Original --
p:before{content: "calc(15+15)" calc(15 + 15)}
-- Results --
Majority:
cleancss csso ----
p:before{content:"calc(15+15)" calc(15 + 15)}
Dissent group 1:
cssnano ----
p:before{content:"30" 30}
Dissent group 2:
crass ----
p:before{content:"calc(15+15)" 30}

My version of cssnano does not treat strings in any special way. Hmm.

Trick or treat?

Let's see if I can goad any minifier into string concatenation.

-- Original --
p { content: calc("hello," + " javascript!") }
-- Results --
Majority:
cleancss cssnano csso ----
p{content:calc("hello," + " javascript!")}
Dissent group 1:
crass ----
Errored out!

Crass errored out with a parse error. The others regurgitate, which is what I'd like to have, in addition to a warning.

Getting saner

Time to leave calc() behind. Let's try a couple of bits of valid CSS, for a change.

Level with me!

-- Original --
p{z-index:100}
a{z-index:200}

Try this exercise for yourself. How would you optimize this?

Let's see how the others did.

-- Results --
Majority:
cleancss crass csso ----
p{z-index:100}
a{z-index:200}
Dissent group 1:
cssnano ----
p{z-index:1}
a{z-index:2}

You may be cheering for cssnano here, but this very thing wrecked my nav bar when interacting with Amazon advertising. This is a valid optimization if the CSS given is the only source of z-indexes. If there are other CSS files or other styles, this might fizzle. I believe cssnano can be set not to perform this optimization.

Twisted mind

How about angles? I'm sure you remember the old trick from MATH 101, right? You can subtract 360 degrees from any angle and you get the same angle, yes?

-- Original --
p { transform: rotate(1801deg) }
-- Results --
Majority:
cleancss crass cssnano csso ----
p{transform:rotate(1801deg)}

I'm actually siding with the minifiers here. It isn't that the minifiers forgot the trick. What if you want to perform two animated rotations in one second? If the CSS minifiers optimized this to 0 degrees it would be a pretty wimpy rotation, no?

Closing thoughts

Well, that was a fiesta of broken CSS. I had my fun and the end of the world is postponed. For now. I'm out of time for the blog for today. I guess the rest will have to wait until next week. Next up: Maybe more valid CSS, for a change. Media queries? More valid, yet absurd CSS. Unspeakable things. Sound? URLs?

Guess what? Corrections welcome! ^_^

I'd really love to know why the pluses get eaten, but only sometimes.

I'm for hire! My CV is here and my email is in the footer!

I'd like to issue an apology for writing "Le Sigh" on my site in my previous post. What I had actually intended to write was: "Le Sigh, Le Sigh, Le Sigh". I'm sorry for falling short in my duties.

I always wanted to write a sob story of an article about how PayPal froze my account and took all my money. Those are always so exciting! Please make my dream of writing that article a reality and try out this snazzy new PayPal donate button!

Donate

(Donors get to hint at next topics I should write about and, if they want, they get early access to articles too!)

The best taste in the world is the taste of the hand that feeds me. So good.

Le sigh.


Past: More missed chances: What minifiers also leave behind