"Async" CSS without JavaScript
Keith Clark posted recently about loading CSS as early as possible, without the browser refusing to render anything until it downloaded it. 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 relies on JS. That gets my hackles up, largely because I am bad at JavaScript, but there are 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 available 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've got our 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>
Just like putting <script>
at the bottom, the <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"
when there's no printer connected, media="(min-width:500px)"
on a smartphone, ormedia="do-not-download-this-means-you"
. This is because one could connect a printer, or rotate their phone, and all of a sudden media
does apply. Dunno about that last one.
(Maybe the @import
statement with media queries can work here? That shouldn't trigger the preloader, and thus allow 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 (I would have used CodePen, but I needed a clean waterfall chart. Sorry guys!) and ran them on WebPageTest:
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 do vote on its Bugzilla here, but IE considers it a wontfix. (I did my own testing and the async version doesn't seem to make any difference in IE, so at least we're not making things worse? More testing needed.)
At this point, we might as well just use the Chromium-only <link rel="subresource">
instead since the browsers it works in are about the same (well, sans Safari) and is 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 as fast as possible? Something like:
<script src="stylesheet.css" async defer></script>
<object data="stylesheet.css" type="text/css"></object>
<img src="stylesheet.css">
The first attempt with <script>
is the only other resource browsers will fetch in the <head>
quickly. But this is dangerous. Even if the JS parser didn't find anything to execute in there, it would still waste resources trying to. And it's only slightly more robust than using a JS loader anyway, since both fail in largely the same situations.
Using <object>
in the <head>
is an ancient preloading technique, since HTML 4.01 allowed it for arcane purposes. But that's disallowed and broken with the new HTML5 parsers.
Using <img>
is pretty hacky, and according to this 2010 phpied blog post, Firefox uses a separate cache for images, so there's another no-go. And of course, we can't put an <img>
in the <head>
.
The Link
header
This is just an HTTP header with identical functionality to the <link>
element. Like this:
Link: <style.css>; rel="stylesheet"
It 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 ask around/test for browser support (I know Firefox and old Opera/Presto do it), and if it blocks.
Just shove it in the <body>
While I was unknowingly retracing The Filament Group's steps on async CSS, I happened upon Yoav Weiss's blog 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 simple as that?
<body>
<link rel="stylesheet" href="style.css">
<!-- more such nonsense which is unimportant -->
</body>
We'll 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 | |
Android | |
Presto | Who can tell? |
Do we have a standard for this?
As far as "please just let me mark a stylesheet as nonblocking," the W3C had the Resource Priorities standard. With it canceled, it's no good to us 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>
<!--[if IE]>
<link rel="stylesheet" href="style.css"> <!-- blocking, but what else can ya do? -->
<![endif]-->
</head>
<body>
<!--[if !IE]> -->
<link rel="stylesheet" href="style.css" lazyload>
<!-- <![endif]-->
</body>
But what about IE 10? It's got a full 1.29% usage share according to Caniuse, and it stopped supporting conditional comments. EHHHHHHHH. So close. For it and the long tail of niche/older browsers that never quite die off, this might be an acceptable loss to you. After all, it doesn't break, it just doesn't work as fast as we would like.
The future
When HTTP/2 hits, we can Server-Push to start loading CSS with the HTML simultaneously, and then we can just shove the <link>
wherever the hell we feel like it. You could try messing around with that part today, if you like. The other good part is a new request in HTTP/2 is peanuts. Of course, we'll still have to support all the older HTTP 1.X clients...
For other upcoming tech, we have that Resource Hints spec, and those shiny new HTML Imports can import styles, and they don't block! Too bad Mozilla isn't big on them.
In conclusion
I need a drink.
No problem of course! That's sorta what Debug View is for, but you need to be logged in for that (or PRO) so it doesn't work in all situations.
Thank you for this. Now I would like to know more about these particular cases, but I don't have the time now to go as extensive as I'd like to:
I did a couple of quick tests with my own site though:
index.html is regular head, index-css1.html is beginning of body and index-css2.html is end of body with media="none".
@Merri Oh, wonderful! That IE test is very interesting. We seem to have a tradeoff between time to first render and document complete, but if we use the async CSS for non-critical things, that might be worthwhile.
I've been working on a more rigorous set of tests which I'll offer as a download eventually. My current setup has some issues with 404s in the font file, etc.
That extra delay in IE11 is caused by transition rules in the CSS, so it is more of an issue with the CSS. So document complete is essentially the same as with the two other cases.
This probably doesn't cover a whole bunch of use cases but wouldn't it be easier to append a link node to head when the css resource is required? I'm thinking from a JS heavy single page web-app type situation where you don't need a component/route css if/until a user access it.
No. And it shouldn't even be suggested. Too many lazy Chrome only stuff. It's becoming the new IE6, and not in a good way (in case someone thinks there was an IE6 "good way")
@egojab Hence the rest of the article
@mbudm Oh yeah, for a JS app, a smart loader is the way to go. This is more for static-ish web pages.
Nice write-up! Also I am very fond of the term "byte burglar" :)
@ryanmcnz Thank you. Turns out every single browser calls it something different, so what's one more?