← All writing
Craft · · 8 min

Service workers on a brochure site

The case for this isn't offline. Half the audience gets nothing anyway, and I still wouldn't cache the HTML on a site I'm handing over.

JavaScript Performance Cross-Browser Compatibility

Client site. Five pages, a contact form, a map, and a phone number. It gets updated when the prices change, which is about twice a year.

I put a service worker on it a few weeks ago, on my own time, mostly because I wanted to see how it felt. It works. The site loads instantly on a repeat visit and it does something reasonable with no connection at all.

What I’ve been chewing on since is whether that was a good idea, and I want to actually answer that rather than do the thing where you write a tutorial and let the enthusiasm imply the answer.

Nobody’s on a plane

I went in thinking about offline, because that’s how this is always sold. Offline first. Works on a plane.

Nobody is reading a plumber’s about page on a plane. That’s not a case. If I’m honest about who visits this site and why, the whole audience is people who searched for a service in their town, arrived, wanted a phone number or an idea of the price, and left. Total time on site under a minute. Genuinely offline is about zero of them.

But offline isn’t the interesting state, and this is the bit I hadn’t properly understood.

Offline isn't the case. The case is the train, where you're connected to something that isn't answering.

A phone with two bars, or on a train, or in a parking garage, is not offline. It’s connected to a network that will eventually respond, so the browser waits, and the request hangs for 15 or 20 seconds before anything happens at all. That’s the state most people’s bad experience of the web actually is, and it’s much worse than being properly offline, because at least offline fails fast.

A cached page renders while that’s going on. That’s the win, and it took me putting the phone in an elevator to notice it.

Sorry, Safari

Now the part that has to be said out loud in 2016, before anyone spends a day on this.

Service workers are in Chrome, Firefox and Opera. They are not in Safari, and they are not in iOS Safari, and every browser on iOS is Safari underneath. They aren’t in Edge yet either. So depending on the client, somewhere between a third and two thirds of the people visiting get precisely nothing from this work, and that includes most of the phones in the country for the sort of business where the audience skews older and iPhone-heavy.

That’s fine. It’s genuinely fine, and I want to be clear I’m not complaining, because this is progressive enhancement working exactly as it’s supposed to: the site is a normal site, it works for everybody, and some people additionally get a faster one. Nothing breaks for the rest.

But it changes the math on whether to spend the afternoon. If I’d told the client I was going to bill for a performance improvement that half their customers can’t receive, I’d have felt a bit odd about it.

Thirty lines and no more

Cache the handful of things the site is made of when the worker installs, then serve those from cache and go to the network for everything else.

sw.js
var CACHE = 'site-v3';
var SHELL = [
  '/css/main.css',
  '/js/main.js',
  '/img/logo.svg',
  '/fonts/source-sans.woff2',
  '/offline/'
];

self.addEventListener('install', function (e) {
  e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(SHELL); }));
});

self.addEventListener('activate', function (e) {
  e.waitUntil(caches.keys().then(function (keys) {
    return Promise.all(keys.filter(function (k) { return k !== CACHE; })
                           .map(function (k) { return caches.delete(k); }));
  }));
});

self.addEventListener('fetch', function (e) {
  if (e.request.mode === 'navigate') {
    e.respondWith(fetch(e.request).catch(function () {
      return caches.match('/offline/');
    }));
    return;
  }
  e.respondWith(caches.match(e.request).then(function (r) {
    return r || fetch(e.request);
  }));
});
JavaScript

Note what that does and doesn’t do. Static assets come from the cache. Pages always go to the network, and only if the network fails do you get a fallback page. I’ll come back to why, because that’s the whole judgment call.

The activate handler deleting old caches is not optional, incidentally. Bump site-v3 to site-v4 and forget that, and the user is carrying both copies around forever.

The best part is the cheapest part

The thing I’d actually recommend to anyone with a small client site, before any of the rest of it, is the fallback page. It’s the cheapest part and it’s the only part with an obvious human benefit.

The default is the browser’s error page, which for a business is a dead end. The alternative is a page you wrote, cached in advance, that says the connection has gone and here is the phone number, here is the address, here are the opening hours. Which is, when you think about it, most of what anybody came for.

Somebody standing outside the shop with no signal, wondering if it’s open, is a real person and a plausible one, and they now get the answer. That’s a better argument than anything about milliseconds.

Caching is a promise

Caching is a promise that data won’t change, and I’ve just made that promise on behalf of a site nobody watches.

That’s precisely why the fetch handler above doesn’t cache pages. On a site that changes twice a year, “twice a year” is exactly when it matters, and the failure mode is a customer reading last year’s prices or ringing a number that’s been disconnected, in a way nobody will notice for months because there’s nobody looking.

There’s a stale-while-revalidate pattern that softens this: serve the cached page instantly, fetch the fresh one in the background for next time. It’s a lovely pattern and I use it on things I maintain. On a site I’m handing over, “the visitor always sees one version behind” is a sentence I’d have to say out loud to the client, and I don’t think they’d like it.

The other cost is subtler. I’ve added a piece of software with a lifecycle, a cache, and a versioning scheme, to a site whose entire selling point was that it was five HTML pages and would need nothing from anybody. If it goes wrong in 18 months, the person who has to work out why is not going to be me, and the symptom will be “the website is showing old information,” which is about as hard to diagnose from a phone call as anything gets.

The verdict

My honest position, which I reserve the right to revise:

Yes to the offline fallback page. Cheap, useful, no staleness risk because it only ever appears when the alternative is an error.

Yes to caching static assets, with the version bump built into the deploy so I can’t forget it. This is the bit that actually makes the site feel instant and the staleness risk is low, because CSS going one version stale for a day is not a business problem.

No to caching HTML on a site I’m walking away from. Not because it doesn’t work. Because the thing that goes wrong is invisible and lands on somebody who can’t fix it, and I’ve decided I care more about that than about the second I’d save.

And no to the rest of it, the install banner and the push notifications, on a five-page site for a local business. Nobody wants that site on their home screen. I’d find it slightly funny if they did.

Who’s holding the bag

The general shape of what I learned, which I suspect applies beyond this: the interesting question about a new capability isn’t what it enables, it’s what it costs the person who inherits it. Service workers are properly good and I’m glad they exist and I’ll use them more as the support fills in.

I’ve just noticed that every nice thing this technology does for me is a thing I’m confident I could still explain to somebody in two years, and everything I turned off is a thing I couldn’t 😅

Read similar posts
5 min

jQuery for one selector

Most of what jQuery was invented to fix has been fixed in the browsers, and a lot of us are still loading the whole library to avoid typing a slightly longer method name.

8 min

Five pages and a contact form

Most of the web is brochures, and a brochure doesn't need a client-side runtime, a router, or a build pipeline that stops working the moment you look away from it.