Mission Critical: optimizing CSS for CDN

Here at Filament Group, we recommend and make use of the critical CSS approach on many of our projects. Frequently those projects utilize a CDN for delivery, and we need to handle each particular implementation a little differently to accommodate different CDNs and their features. Here we’ll summarize that challenge and provide some direction on how to address it for a few example CDNs.

Overview

Permalink to 'Overview'

Critical CSS is a technique used to get the above-the-fold CSS for an HTML page into the first 14kb window of the page request. This allows the browser to do a meaningful render of the content faster. The full CSS, i.e. the CSS for the whole page including anything above-the-fold, is loaded by the page later and hopefully cached by the browser.

Once the full CSS has been cached by the browser it will be available locally on all subsequent page requests. As a result it’s much better to serve a page that references the cached file (via a link element) and avoid including the duplicate above-the-fold CSS. With the full CSS cached, the extra CSS won’t increase the speed of the first render and constitutes a waste of transfered bytes, parsing, and possibly extra rendering passes.

This results in at least two pages for every URL. One page with the above-the-fold and full CSS for the first request when the full CSS is not cached, and one with just the full CSS for subsequent requests when it is cached.

If you are using a CDN to cache static content, the CDN needs to cache both pages and serve them appropriately according to the browser cache state. Otherwise, the content of the cached page may depend on the browser cache of the first request made to the CDN.

Edgy Content

Permalink to 'Edgy Content'

Caching page content on the CDN edge servers has a few benefits. First and foremost, if the edge server is geographically closer to the client than the origin server, the CDN is likely to reduce the time it takes for the browser get the bytes it needs to begin rendering. It also reduces load on the origin server.

In our work with clients we’ve found this to be provide a big benefit to rendering times when the testing from different locations.

Conditional Page Rendering

Permalink to 'Conditional Page Rendering'

Knowing, on the server, when the full CSS is cached by the browser is often done via a heuristic. One approach we’ve been experimenting with lately is to use an HttpOnly cookie served in the response to a request for the full CSS file. Assuming the browser will cache the full CSS response, the server can avoid including the above-the-fold CSS in later page requests by checking for the cookie.

The following Apache configuration snippet sets the cookie based on a request for the full CSS file.

<IfModule mod_headers.c>
    <If "%{HTTP_COOKIE} !~ /visited\=VERSION/ && %{DOCUMENT_URI} =~ /full\.css/">
        Header set Set-Cookie "visited=VERSION; Path=/; HttpOnly"
    </If>
</IfModule>

Note the cache breaker VERSION for the cookie value. When deploying a new version of the CSS this ensures that clients with a stale cookie value will get the critical CSS while it updates the full CSS and cookie to the new version in the background.

In addition, the following snippet of code is an example of what would be included in the head of each page. It uses Apache’s Server Side Includes to leverage the example cookie based approach. Note that, this can be done in any standard templating language with access to cookie state.


<!--#if expr="$HTTP_COOKIE=/visited\=VERSION/"-->
  <link rel="stylesheet" href="/path/to/full.css?VERSION">
<!--#else -->
  <style>
    /* inlined critical css */
  </style>

  <!-- preload full css and apply on load.
          if unsupported, rel=preload links with as=style are polyfilled via js -->
  <link rel="preload"
           href="/path/to/full.css?VERSION"
           as="style"
           onload="this.onload=null;this.rel='stylesheet'"/>
  <noscript>
    <link rel="stylesheet" href="/path/to/full.css?VERSION">
  </noscript>
<!--#endif -->

Together, these bits of code will produce two pages depending on the state of the visited cookie and any CDN caching the content must cache and respond with both pages according to the visited cookie.

CDN Specific Configuration

Permalink to 'CDN Specific Configuration'

On the CDN we need the edge servers to recoginize that the pages vary based on the visited cookie value and cache both versions accordingly. Below we have collected the basics of how to do this for Akamai, CloudFront, and Fastly.

Akamai

Permalink to 'Akamai'

In Akamai’s web interface the cache key can be modified by customizing a particular “property”. In the Property Manager Editor, either in a default rule or some other qualified rule that matches the content to be cached add the following “Cache ID Modification” behavior:

akamai config

Note that the cookie is “Not Required for Caching” because we want the initial requests that will not have the cookie to be cached.

CloudFront (AWS)

Permalink to 'CloudFront (AWS)'

Cloudfront can be customized to forward a whitelist of cookies to the server and cache based on each of the cookie values. After selecting a distribution, edit one of the behaviors for the distribution, and change the “Forward Cookies” to Whitelist and add the visited cookie.

cloudfront config

It’s important to note that, according to the documentation, CloudFront cannot be configured to modify the cache key based on some subset of those cookies. As a result, if you are using other cookies that need to be forwarded to the origin server from the CDN then you may have more pages cached based on the combinations of cookie values even if you don’t vary the page based on the other cookies.

Fastly

Permalink to 'Fastly'

Fastly uses the Varnish Configuration Language to customize the behavior of edge servers. You can use the following snippet to modify the cache key to account for the visited cookie. It uses a regular expression match and appends the result to the cache key.

sub vcl_hash {
  set req.hash += req.url;
  set req.hash += req.http.host;
  set req.hash += regsub(req.http.cookie, ".*visited=([^;]+);.*", "\1");
#FASTLY hash
  return (hash);
}

For more see their excellent VCL cache key documentation.

All blog posts