Qubyte Codes

Putting back the service worker

Published

In the last post about this blog I wrote about why I removed the service worker which made this blog a progressive web application.

The way my blog handles CSS predates the wide availability of service workers. Since CSS link tags are blocking, it's good to give CSS a long cache time. In order to do this, but still deliver fresh CSS without readers having to wait for it, I give the CSS file a URI including a hash of its content. If the CSS is updated, its URI changes, as does the href of the CSS link tag in each page of this blog.

The server sends this header along with CSS it serves:

Cache-Control: max-age=315360000, public, immutable

The immutable tells the browser the file will never change while within the max-age (so I make it very long). Chrome doesn't support immutable, but does exhibit similar behaviour. The public allows proxies to similarly cache the response.

I instructed the browser not to cache HTML at all. Since the HTML was always fresh, updated CSS would be loaded at most once on each change. The server sends this header along with HTML:

Cache-Control: no-cache

Which forces it to make a fresh request for HTML each time. These headers are still in place now (even since the move to Netlify).

Unfortunately this didn't mesh well with the caching strategy of the service worker I added. The worker would download the entire blog, HTML and CSS, the first time a user navigated to it. The service worker was generated by sw-precache, which I had set up to be generated every time the blog was updated. It worked out what to add and remove from its cache based on file hashes. Since an update to CSS meant changing the address in a link header in every HTML page, it removed not only the CSS, but all the HTML any time the CSS changed. Worse, it proactively downloaded all the updated files.

The net effect was minor CSS changes triggering a mass download of my blog. This wasn't a good use of server or browser resources, so I removed the worker.

I recently attended a Homebrew Website Club held by Jeremy Keith. By chance he'd written a blog post on this issue the day before, in which he provides a minimum viable service worker.

This service worker caches everything lazily (no precache). When an HTML page is requested, it always tries the network first for a fresh copy, and falls back to the cache when necessary. For everything else it hits the cache first, but gets an update via the network in the background.

This resolves the CSS issue very nicely. Old CSS and HTML will be cached, so if your Southern Rail train is stuck in a tunnel, you can still read that blog entry about mixins you saw earlier and are now bored enough to take another look at. If you're stuck outside of a tunnel and I've updated the CSS, the request for fresh HTML will succeed, which will also bring in and cache the fresh CSS! For things like images, which will probably never change, this also works well. If a change is urgent to any file, it can still be given a fresh URL to cache bust it (though I doubt this'll ever be necessary).

I'll still need to do some work though. As mentioned in that post, cleanup isn't addressed. I'm happy for HTML to be cached indefinitely, but for the CSS one way to clean it up might be to remove it once it is older than every HTML entry in the cache. For particularly large resources such as images, a relatively short cache time and good alt-text might be a good approach...

In the meantime, I've borrowed the service worker code from that post mostly unchanged (most changes are just to align it with the style enforced by my ESLint config). The one small addition is to allow server sent events (EventSource) connections. I use these in development to hot-reload my blog when changes are made. The original script ignores all but GET requests:

if (request.method !== 'GET') {
  return;
}

I've extended this to also ignore server sent event connections:

const acceptHeader = request.headers.get('Accept');

if (request.method !== 'GET' || acceptHeader.includes('text/event-stream')) {
  return;
}

You can see the current service worker code I'm using here.

I had to add some CSS to handle superscripts, such as the one used for this footnote.

Perhaps the blog will be a different shade of beige...

Backlinks