Using localStorage to improve critical CSS rendering

If you are interested web performance you might have read about the Critical Rendering Path. The idea behind it is, that you send the CSS (and JS) as fast as possible so the browser can start rendering the visible content first. Google has introduced this as one of the criteria for Pagespeed. After recently listing to a discussion on the Workingdraft podcast (German), I decided to give in and try to improve this site.

Typically one would inline critical CSS to save HTTP requests. I didn’t want to sift through the CSS, which is based on Inuit CSS. Inlining all CSS would loose the advantage of caching. But with HTML5 localStorage you can store CSS in the browser. To know server-side that CSS has been stored, I had to use cookies. A checksum is used to detect any changes to the CSS.

Unfortunately it was not as easy as I had hoped. Injecting CSS caused problems in browser that do not support window.matchMedia, e.g. iOS 4, Android browser, IE 8/9. Newer Android browser versions seem to support matchMedia, but still caused problems. So I feature detect support for localStorage and matchMedia. And additionally browser sniff for the Android browser. Storing the CSS in localStorage is postponed to the end of the page, to avoid blocking rendering. This solution is far from perfect, but good enough for my purposes.

[update] I did a CSS cleanup and the problem with matchMedia disappeared. Probably there was some string escaping problem. So a check for matchMedia support is not needed anymore.

This example requires PHP 5.4 or later. You do know that PHP 5.3 has reached End of Life, right? LocalStorage depends on Javascript, so without JS you have to load the CSS as
usual with a meta link. Unfortunately, in this case, CSS will be send twice, inline and as an external file. If cookies are not enabled, CSS will be send inline with every request.

So go ahead and try this technique. But you should test it on real devices to
find any possible negative side effects.

Code in head


<?php
$stylesheetPath = '/css/style.min.css';
$stylesheetCSS = file_get_contents($_SERVER['DOCUMENT_ROOT'] . $stylesheetPath);
$stylesheetCSS = preg_replace(
    "/[\r\n]/m",
    '',
    str_replace(
        ["'",'"'],
        ['\\x27','\\x22'],
        trim($stylesheetCSS)
    )
);
$stylesheetHash = hash('md5', $styleContent);
$stylesheetURI = $stylesheetPath . '?h=' . $stylesheetHash;
?>

<script>
var stylesheetHash = false;
var stylesheetCSS = false;
var injectCSS = false;
if ("localStorage" in window) {
    injectCSS = true;
}

<?php if (empty($_COOKIE['stylesheet']) or $_COOKIE['stylesheet'] !== $stylesheetHash): ?>
stylesheetHash = "<?= $stylesheetHash; ?>";
stylesheetCSS = "<?= $stylesheetCSS; ?>";
<?php endif; ?>

if (injectCSS) {
    // Get CSS from local storage
    if (!stylesheetCSS) {
        stylesheetStorage = window.localStorage.getItem("stylesheet");
        if (stylesheetStorage) {
            stylesheetCSS = stylesheetStorage;
        }
    }

    if (stylesheetCSS) {
        // Inject CSS
        document.write("<style>" + stylesheetCSS + "</style>");
    } else {
        // Use fallback
        injectCSS = false;
    }
}

// Fallback
if (!injectCSS) {
    document.write('<link href="<?= $stylesheetURI; ?>" rel="stylesheet" type="text/css" />');
}
</script>

<noscript>
<link href="<?= $stylesheetURI; ?>" rel="stylesheet" type="text/css" />
</noscript>

Code in footer


<script>
if (stylesheetHash) {
    var date = new Date();
    date.setTime(date.getTime()+(30*24*60*60*1000));
    document.cookie = 'stylesheet=' + stylesheetHash + '; expires=' + date.toGMTString() + '; path=/;'
}
if (injectCSS && stylesheetCSS) {
    window.localStorage.setItem('stylesheet', stylesheetCSS);
}
</script>

[update] Instead of using a md5 hash, you can also resort to the last modification time. This should be a bit faster. Type casting the time to a string avoids undesired side effects, because PHP is weakly typed.


$fileStat = stat($_SERVER['DOCUMENT_ROOT'] . $stylesheetPath);
$styleHash = (string) $fileStat['mtime'];