Asynchronous CSS delivery for fault tolerance
To prevent pages loading as unstyled content which then has to be redrawn when the CSS has loaded, browsers do not render anything to screen until all the external CSS resources referenced by <link rel="stylesheet">
elements in the page have been loaded. With a reliable network connection this is not an issue but if - for whatever reason - these resources are taking time to load then users will see a blank page until these requests either succeed or fail.
What causes this kind of failure?
The normal way of adding CSS into a page is to use <link>
elements in the <head>
of the document to reference external CSS files:
<html>
<head>
<link rel="stylesheet" href="/path/to/external.css"/>
…
When a browser encounters tags like this it will request the CSS file over the network but will not render any content to screen until all requests for CSS have completed. If the requests succeed then the browser is able to start rendering the content with the CSS applied. If any, or all, of the requests fail then it will be able to render the content without the CSS contained in those files.
Providing these requests complete quickly there is little impact on the perceived loading time of the page. The problem comes if these requests take some time to reach their completed state and it's in this case the browser will block and not render any content.
Why do requests take time to complete?
There are a number of reasons why network requests may take time to complete, for example:
- Slow network connection
- Intermittent network connection
- Congested network connection
- Misconfigured networking equipment
- Overloaded server
- Misconfigured server
This is by no means an exhaustive list and most of the causes are beyond your control. The end result is the same: the network request is trying to complete for a significant period of time before either completing itself or it is timed out by the browser before it completes.
Inline core CSS into the head of the document
Inlining CSS into the head of the document is already a performance best practice but, from a performance point of view, the focus is very much on delivering the CSS required to render content "above the fold".
As there is no meaningful way of determining where the "fold" is on a web page there is also no meaningful way to determine which CSS is required to render it. Instead the CSS inlined in the head of the document should be enough to render the page sufficiently to allow it to be usable.
<html>
<head>
<style>body{background-color:#1e163d;color:#32b2d7;font-family:sans-serif;font-size:100%;margin:1em 2.5%}@media all and (max-width:13.75em){body…<style>
…
Provided there are no other <link rel="stylesheet">
elements in the page the browser will have enough information to render the page with this inlined CSS.
Delayed load of enhanced CSS
To prevent significantly increasing the size of the page it is important to keep the inlined CSS to a minimum. The rest of the CSS required to fully render the page can be included in an external file which is injected using inline JavaScript.
Whilst the inlined CSS will only contain the basics required to make the core page functionality usable, the enhanced CSS will make the page much easier to use and significantly more visually appealing.
By using JavaScript we are able to delay the application of the enhanced CSS to give the browser the chance to render the page with the inline CSS which means it can be used even if the enhanced CSS is taking time to load, or even if it does not load at all. By using JavaScript inline with the page it means that the browser does not have to request an additional resource over the network.
When a <link>
element is injected into the DOM before the DOMContentLoaded
event the browser will block until the request completes. As we are trying to defend against a situation where the request for the CSS does not complete we need to use an asynchronous request and inject the contents into the page upon completion. Using an asynchronous request means we can initiate the request for the enhanced CSS at the earliest possible point - even before DOMContentLoaded
- without blocking the browser. However, as support for XMLHttpRequest
is not universal we need to feature detect this and fall back to the onload
event for maximum browser support.
With our fallback to the load event we also resort to injecting a <link>
element as this is the most reliable cross browser method of adding styles. Unfortunately this injection can block the event. To avoid this we detach the injection of the <link>
element from the event by using a time out.
Bringing it together, the code for this is as shown below:
(function () {
var main_css = "/path/to/enhanced.css",
use_fallback = true; // If XMLHttpRequest is an old version we use the fallback.
if (typeof XMLHttpRequest !== 'undefined') {
// We might be able to do this asynchronously.
var request = new XMLHttpRequest;
if ('onload' in request) {
// This is a new version of XMLHttpRequest so crack on.
use_fallback = false;
request.open('GET', main_css);
request.onload = function() {
if (request.status >= 200 && request.status < 400){
var s = document.createElement('style'),
css = request.responseText;
s.appendChild(document.createTextNode(css));
document.getElementsByTagName('head')[0].appendChild(s);
}
};
request.send();
}
}
if (use_fallback) {
// No good XMLHttpRequest support so fallback to piggybacking the onload event.
var old_onload = window.onload;
var l = document.createElement('link');
l.rel = 'stylesheet';
l.href = main_css;
// DRY - define this once and call it from within the conditional check.
function injectCSS() {
// We wrap this in a setTimeout to prevent it blocking the event if the CSS fails to load.
window.setTimeout(function () {
document.getElementsByTagName('head')[0].appendChild(l);
}, 100);
}
if (typeof window.onload !== 'function') {
// There's no existing onload handler so run our function.
window.onload = injectCSS;
} else {
// Call the old onload handler before running our function.
window.onload = function() {
if (old_onload) {
old_onload();
}
injectCSS();
}
}
}
}());
JavaScript dependency
This technique does mean that some of the CSS is now dependent on JavaScript. However, it is not a complete dependency as the inlined CSS in the head of the document will still be applied when JavaScript does not run and the page is still usable when this is applied. Current estimates indicate that this is only likely to affect around 1% of users. We are also attempting to inject the CSS using a method with good cross browser support (rather than cutting the mustard) in an attempt to minimise this dependency.
Due to the complexity introduced by this JavaScript dependency this technique is not universally suitable. However, if you are looking to provide maximum reliability in hostile network environments the benefits may outweigh the cost of this additional complexity.
Conclusion
By making your CSS fault tolerant in this way you are removing a potential point of failure. Assuming your JavaScript is also fault tolerant this means that your page is at least usable with a single HTTP request.
However, this technique adds complexity and ultimately fights against the browser's inherent behavior so should be used with caution as the costs could easily outweigh the potential benefits.