I wanted to figure out the fastest way to load non-critical CSS so that the impact on initial page drawing is minimal.
TL;DR: Here's the solution I ended up with: https://github.com/filamentgroup/loadCSS/
For async JavaScript file requests, we have the async
attribute to make this easy, but CSS file requests have no similar standard mechanism (at least, none that will still apply the CSS after loading - here are some async CSS loading conditions that do apply when CSS is inapplicable to media: https://gist.github.com/igrigorik/2935269#file-notes-md ).
Seems there are a couple ways to load and apply a CSS file in a non-blocking manner:
- A) Use an ordinary
link
element to reference the stylesheet, and place it at the end of the HTML document, after all the content. (This is what Google recommends here https://developers.google.com/speed/docs/insights/PrioritizeVisibleContent ) - B) Fetch it asynchronously with some inline JavaScript from the
head
of the page, by appending alink
element to page dynamically. (To ensure it's async, I'm setting thelink
's media to a non-applicable media query, then toggling it back after the request goes out).
I suspected B
would initiate the request sooner, since A
would be dependent on the size of the content in the document - parsing it in entireity before kicking off the request.
In this case, sooner would be ideal to reduce impact on our page load process, since there could be minor reflows that are triggered by the styles in the non-critical CSS file (ideally, the critical CSS would have all reflow-triggering styles in it, but that so far, that's been hard to pull off across the breakpoints of a responsive design, and given that the initial inlined CSS should be kept very small to fit into the first round trip from the server).
I made some test pages, and they do seem to confirm that B will load the CSS sooner (B
requests the css file after around 60-70ms
whereas A
usually requests it around 130-200ms
). It's consistently about twice as fast.
- Demo A (static
link
at end of page): http://scottjehl.com/test/loadingcss/fromhtml.html - Demo B (dynamically loaded
link
): http://scottjehl.com/test/loadingcss/fromscript.html
B comes with the additional benefits of qualifying the request based on whatever conditions we care to test too, which is nice.
- Update noted here: https://gist.github.com/scottjehl/87176715419617ae6994#comment-1243423
- Another note: @adactio helpfully pointed out that the approach would be a bit better if the non-critical CSS still loaded for non-JS environments. Wrapping a
link
to that file in anoscript
tag and placing it at the end of the page would be a nice way to address that. - Update: loadCSS has its own home now! https://github.com/filamentgroup/loadCSS/
We've been using this approach for nearly a year now in production on the Guardian responsive site, and have learnt some of the issues mentioned in this thread along the way. Our current approach is as follows:
In the head of the document:
<meta>
)<link>
for older browsers<style>
elementhttps://github.com/guardian/frontend/blob/master/common/app/views/fragments/commonCssSetup.scala.html
In the foot of the document:
-- Inline into
<style>
in the head-- Cache into localStorage for next page load
https://github.com/guardian/frontend/blob/master/common/app/views/fragments/loadCss.scala.html
The reason we use localStorage is to be more resilient against single point of failures. I.e. we now have the entire styles of the page within the initial html payload (a single HTTP request). You could argue that the same could be said if the global styles are in the HTTP cache, but this is rarely the case on mobile. Hopefully the ServiceWorker spec will standardise such hacking and allow us to design for offline first.
With regards to the "what are critical styles" question, ours are a mixture of breakpoints to avoid FOUC on larger devices, but largely covering the header, navigation, base layout and content typography. Due to the file size restrictions (we enforce max 14kb gzipped), we've found that we can't have a single head file that accommodates every section/content type across the site. So have resorted to maintaining ~3-4 head files to fine tune the experience. This does result in duplicate styles in your global stylesheet, but the trade-off is worth it, you just have to be careful of the cascade.
I hoping that a combination of HTTP/2 and the Resource priorities API are going to make these techniques anti-patterns in the future 😉. Thanks for opening this thread @scottjehl it's very interesting to hear others experiments.