Keith Clark posted recently about loading CSS as early as possible, without the browser refusing to render until it finished. Like <script async>, but for stylesheets. Maximum speed.

His solution sadly didn’t withstand the first salvo of browser testing, which The Filament Group discovered some time ago. Hence, their LoadCSS library, which is the littlest style loader they could make that doesn’t sacrifice robustness.

But it needs JavaScript. That gets my hackles up, because I am bad at JS, but also those “separation of concerns” and “don’t introduce layer dependencies” thingos. And the more we cooperate with the lookahead pre-parser/preload scanner/byte burglar, the better. With JS loaders, the preloader can fill all connections with images and such, leaving your CSS stuck behind them.

First whack: Async CSS with media="bogus" and a <link> at the foot

Say we have HTML structured like this:

  <head>
  <!-- unimportant nonsense -->
  <link rel="stylesheet" href="style.css" media="bogus"/>
</head>
<body>
  <!-- other unimportant nonsense, such as content -->
  <link rel="stylesheet" href="style.css"/>
</body>

Like <script> at the bottom, <link> before the closing </body> should only start blocking when there’s nothing left to block. Do whatever, browser, you can’t ruin anything.

Meanwhile, way up in the <head>, browsers insist on downloading stylesheets with media attributes they could never fulfill, like media="print" without a printer connected, media="(min-width:500px)" on a smartphone, or media="do-not-download-this-means-you". This is because one could connect a printer, or rotate their phone, and suddenly media does apply. (Dunno about that last one.)

(Maybe the @import statement with media queries can work here? That shouldn’t trigger the preloader, thus allowing the browser to make intelligent fetching decisions, but if it were that simple, surely we’d be using it.)

However, modern browsers don’t block on these unmatching stylesheets. So if we prime the cache with the early, inapplicable-media download, any browser with half-decent networking code should just reuse the file it’s already downloading/cached when it encounters the second <link>.

I whipped up a couple of test pages and ran them on WebPageTest:

The async CSS takes only 0.4 seconds, the regular takes a full second.

These results are awesome: sliced the time right in half! But The Filament Group strikes again — I should have known they’d been there, done that.

When I said “modern browsers” I was telling a lie. Scott Jehl found out Firefox and IE still totally block. Wimp womp. If you want Firefox to fix this, please vote on its Bugzilla here, but IE says wontfix. (I did my own testing and the async version doesn’t seem to make a difference in IE, so at least it doesn’t worsen things? More testing needed.)

At this point, might as well use the Chromium-only <link rel="subresource"> instead, since it’s much less hacky. Which is a nice boost for Chromium folks, and the technique still works in other browsers, if not optimally. But is there another way?

Abusing the browser cache

What if we used something else to prime the cache and start downloading the CSS high in the <head>? Something like:

  • <script src="stylesheet.css" async defer></script>
  • <object data="stylesheet.css" type="text/css"></object>
  • <img src="stylesheet.css">

Scripts are the only other resource browsers will preload in the <head>. But that’s dangerous. Even if the JS parser didn’t find anything to execute, it still wastes resources trying to. And it’s only slightly more robust than a JS loader anyway, since both fail in largely the same situations.

<object> in the <head> is an ancient preloading technique, since HTML 4.01 allowed it for arcane purposes. But that’s broken in the HTML5 parsing algorithm.

Using <img> is pretty hacky, and according to this 2010 phpied blog post, browsers can use separate caches for images, so that’s another no-go. And we can’t put <img> in the <head>.

The Link header

This is an HTTP header identical to the <link> element. Like this:

  Link: <style.css>; rel=stylesheet

Doesn’t get much faster than putting the style before the file. If we could prime caches with this, that would be perfect. I'll need to test for browser support (I know Firefox and old Opera/Presto do it), and if it blocks.

Shove it in the <body>

While I was unknowingly retracing The Filament Group’s steps on async CSS, I happened upon Yoav Weiss’s post:

It seems this trick causes Chrome & Firefox to start the body earlier, and they simply don’t block for body stylesheets.

Could it be as easy as that?

  <body>
  <link rel="stylesheet" href="style.css"/>
  <!-- more such nonsense which is unimportant -->
</body>

Need to test it, of course. I'll fill in this table as I work my way through.

Browser Engine Async in body?
Blink Yes!
Gecko Yes!
Trident
WebKit Yes!
iOS WebKit Yes!
Android
Presto Who can tell?

Do we have a standard for this?

As far as “please let me mark a stylesheet as nonblocking,” the W3C once had Resource Priorities. With that canceled, it’s no good until its successor Resource Hints lands. Or is it!?

Internet Explorer 11 implemented it. And with conditional comments we can support IE 9 and lower:

  <head>
  <!-- blocking, but what else can ya do? -->
  <!--[if IE]>
    <link rel="stylesheet" href="style.css"/>
  <![endif]-->
</head>
<body>
  <!--[if !IE]> -->
    <link rel="stylesheet" href="style.css" lazyload="1"/>
  <!-- <![endif]-->
</body>

But what about IE 10? It stopped supporting conditional comments. EHHHHHHHH. So close. For it and the long tail of niche/older browsers that never die off, this might be an acceptable loss. After all, it doesn’t break, only doesn’t work as fast as we would like.

The future

With HTTP/2, we can Server-Push to load CSS with the HTML simultaneously, and shove the <link> wherever we feel like. The other good part is a new request in HTTP/2 is peanuts. Though, we’ll still have to support all the older HTTP 1.X clients…

<link rel="preload" as="stylesheet"> is the new hotness, but it’s still being ironed out.

In conclusion

I need a drink.


82,962 11 59