Inlining or Caching? Both Please!

Posted by Scott 11/13/2018

Last week, I was finishing a section of my slides for a presentation at the Performance.Now() conference. In it, I was exploring patterns that enable the browser to render a page as fast as possible by including code alongside the initial HTML so that the browser has everything it needs to start rendering the page, without making additional requests.

Our two go-to options to achieve this goal are inlining and server push (more on how we use those), but each has drawbacks: inlining prevents a file from being cached for reuse, and server push is still a bit experimental, with some browser bugs still being worked out. As I was preparing to describe these caveats, I thought, “I wonder if the new Service Worker and Caching APIs could enable caching for inline code.”

As it turns out, they can!

Caching an Inlined File

Let's start with an example. The following code snippet has a style element containing some inline CSS. I've added an ID attribute onto the style element so that it's easy to find it using JavaScript. After the style element, a small piece of JavaScript finds that CSS and uses the new Cache API to store it in a local browser cache (with a content type of text/css) for use on subsequent pages.


<style id="css">
.header { background: #09878}
h1 { font-size: 1.2em; col… }
h2 { margin: 0; }
…
</style>
<script>
if( "caches" in window ){
  var css = document.getElementById("css").innerHTML;
  if( caches ){
    caches.open('static').then(function(cache) {
      cache.put("site.css", new Response( css,
        {headers: {'Content-Type': 'text/css'}}
      ));
    });
  }
}
</script>

After that runs, the inline CSS will be stored in a file called site.css in a local service worker cache. But in order to use that file on subsequent pages, we will need to go a little further.

Rounding this out with a Worker

You might recognize the caches API above if you've worked with Service Workers. Despite its ties to Service Workers however, Caches is actually available in the window global namespace as well, so unlike workers, we are able to use Caches while having access to content from the DOM. That said, in order to ensure a subsequent page on the site will get this local site.css file if the browser requests it, a service worker will still be needed.

Here's a common Service Worker snippet you might already have running on your site: when a file is requested by the browser, the worker looks in local caches before going to the network. With this worker running, any future requests to site.css will find it in local cache and avoid a network request.


(function(){
  'use strict';
  self.addEventListener('fetch', event => {
    let request = event.request;
    let url = new URL(request.url);
    // Ignore non-GET requests
    if (request.method === 'GET') {
      // try local cache first, then go to network
      event.respondWith(
        caches
          .match(request)
          .then(response => { return response || fetch(request); })
      );
    }
  });
})();

We're now ready to handle requests to site.css on subsequent pages of the site.

It's worth noting here that in order to truly take advantage of this pattern in a site's templates, subsequent pages will need to avoid inlining the files and instead reference them externally (using a stylesheet link, a script tag, etc.). To negotiate that in an HTML template, we often set a cookie on the first visit to a site. Then, on subsequent page views, the server can check for that cookie to infer that it's safe to assume the browser has files already in cache. If so, it can reference the files externally instead of inlining them.

If this process is new to you, I'd recommend reading our article Modernizing our Progressive Enhancement Delivery.

A Proof of Concept Demo

To demonstrate the pattern in action, I've created a simple demo page.

Here's how it works: The page linked below includes some inline CSS and a caching function much like the example above. It also installs the service worker code above to check for files in local caches when requests are made. That first demo page contains a link to a second page which references site.css using a regular <link rel="stylesheet" href="site.css"> element. If site.css is loaded locally, which should happen in any service worker supporting browser, the second page will have a green background. If site.css is loaded from the server, the background will be red.

You can try the demo here

Taking it Further

There are some areas that come to mind immediately when thinking about how we can improve on the example above.

Optimizing for Reuse

One improvement we can make is to set up the script to look multiple inline style and script elements using a common selector, and apply custom attributes to these elements to declare their content-type, file path, and cache name for saving their content to cache. With those attributes in place, a script can find and cache many of these inlined files in a single sweep.

That might look like this:


<style data-cache="static" data-type="text/css" data-path="/css/site.css">
  body { background: green; color: #fff; }
  a { color: #fff; }
</style>
<script data-cache="static" data-type="text/javascript" data-path="/js/head.js">
  alert( "This script ran from head.js, which was inlined and local." );
</script>
<script>
if( "caches" in window ){
  function cacheFile( file ){
    caches.open( file.filecache ).then(function( cache ) {
      cache.put( file.filepath, new Response( file.filetext,
        {headers: {'Content-Type': file.filetype }}
      ));
    });
  }

  var toCache = document.querySelectorAll( "[data-cache]" );
  for( var i = 0; i < toCache.length; i++ ){
    var elem = toCache[ i ];
    cacheFile({
      filepath: elem.getAttribute( "data-path" ),
      filetype: elem.getAttribute( "data-type" ),
      filecache: elem.getAttribute( "data-cache" ),
      filetext: elem.innerHTML
    });
  }
}
</script>

Here's a second demo that does just what you see above, with both CSS and Javascript (click through to its second page again to see it work).

Optimizing our Critical CSS Approach

We might also consider ways to use this pattern to improve upon existing critical CSS patterns. For example, we often inline a page's critical CSS, and then load the site's full CSS at a later time. In this workflow, the full CSS file typically overlaps many of the CSS rules with the "critical" subset we already delivered. A better approach might be to load a full version of the CSS that does not contain the critical rules at all, and then use the Caches API to combine the critical and full CSS files to create a full site CSS entry in local cache.

Deliver an Entire Website via HTML??

If we wanted, we could even try using this pattern to deliver an entire front-end codebase in one HTML response, mimicking a fancy web packaging format. Would it be a good idea? Probably not! But it would be fun to experiment.

For now, we'll be brainstorming about other potential uses... and possibly updating our site! Thanks for reading.