Two bars in the parking garage
I was late for something, standing in the garage under our building, trying to get a phone number off a site we built.
Two bars. The page didn’t load and it didn’t fail either. It sat there white with the spinner going. I gave up after what felt like a long time and got the number out of the search listing instead, where it had been sitting the whole time. I went back down later and timed it, because I wanted to know. The better part of 20 seconds before anything at all happened.
So that weekend I put a service worker on a copy of that site to see what it would do. Five pages, a contact form, a map, a phone number, and prices that get changed about twice a year. It took an evening. It works, in the sense that the site now draws instantly on a repeat visit and does something sensible with no connection at all.
Since then I’ve been turning over whether that was a good idea, which is a different question from whether it was fun, and I’m finding those two very hard to pull apart.
Nobody is on a plane
Every article about this sells it on offline. Offline first. Works on a plane.
Nobody is reading a local business’s about page on a plane, come on. If I’m honest about who visits this site, it’s people who searched for a service in their town, arrived, wanted a number or a price, and left. Time on site is well under a minute. The genuinely offline share of that is approximately nobody.
But offline was never the state I was in, and that’s the part I hadn’t understood.
A phone on two bars is not offline. It’s connected to a network that is going to answer eventually, so the browser waits, because waiting is the correct behavior and there is nothing else for it to do. No error, nothing on screen, no way to tell slow from broken. Offline at least fails fast. You get the dinosaur, you know where you stand, you go do something else with your afternoon.
A cached page draws straight through all of that. That’s the actual win, and I had to stand in a garage to notice it.
30 lines, most of them callbacks
It only caches the handful of files the site is made of when the worker installs, serves those out of the cache afterward, and goes to the network for everything else.
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) {
// pages always go to the network, and fall back only when it fails
if (e.request.mode === 'navigate') {
e.respondWith(fetch(e.request).catch(function () {
return caches.match('/offline/');
}));
return;
}
// everything else comes out of the cache if it's in there
e.respondWith(caches.match(e.request).then(function (r) {
return r || fetch(e.request);
}));
});
Worth noticing what that does and doesn’t do. Static assets come out of the cache. Pages always go to the network, and only when the network fails does anybody see the fallback. I’ll come back to that, because it’s the whole judgment call.
The activate handler deleting the old caches is not optional, by the way. Bump site-v3 to site-v4 and forget that block, and every returning visitor is carrying two copies of your stylesheet around forever, then three.
Sorry, Safari
Then the part nobody puts in the tutorials, before anybody spends an afternoon on this.
Service workers are in Chrome, Firefox and Opera. They are not in Safari, they are not in iOS Safari, and every browser on iOS is Safari underneath whatever the icon says. Edge doesn’t have them yet either. So some share of the people visiting get precisely nothing out of this, and for a local business whose customers skew older and iPhone-heavy, it’s more than half of them.
That’s fine. It’s progressive enhancement working exactly the way it’s supposed to, and the accommodation is one line.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
The site is a normal site, it works for everybody, some people also get a faster one, and nothing breaks for the rest. It does change the math on the afternoon though. A performance improvement that most of the audience can’t receive is a strange thing to put on a timesheet against somebody’s account. I don’t think I could have written that line without feeling a little odd about it.
What I wanted in the garage
The part I’d actually recommend to anyone with a small client site, ahead of all the rest of it, is the fallback page. It’s the cheapest thing here and it’s the only one with an obvious benefit to a person.
The default when a navigation fails is the browser’s error page, which for a business is a dead end with a dinosaur on it. 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 hours. Which is, if you look at it honestly, most of what anybody came for in the first place.
That’s the page I wanted in the garage. Not a faster site. The number, in any form, from anywhere.
With one asterisk I didn’t think about until well afterward, which is that none of this exists on a first visit. The worker installs on the way out the door, so every one of these is a repeat-visit feature, and the person standing outside a store at 8 pm wondering whether it’s still open only gets the good version if they’ve been to the site before. Some of them have. I don’t know what share, and I didn’t check before I started, which is going to come up again in a minute.
Caching is a promise
Caching is a promise that data won’t change, and I had just made that promise on behalf of a site nobody is watching.
Which is exactly why the fetch handler up there doesn’t cache pages. On a site that changes twice a year, twice a year is precisely when it matters, and the failure mode is a customer reading last year’s price or calling a number that got disconnected in March, in a way nobody notices for months because there is nobody looking.
There’s a stale-while-revalidate pattern that softens this, where you serve the cached page instantly and fetch the fresh one in the background for next time. It’s a good pattern and I use it on things I still have my hands on. On a site we hand over, “every visitor sees the version before the current one” is a sentence I would have to say out loud to a client, and my sense is that I would not get through it.
The subtler cost is the one I wrote about in June from the other direction. 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 that would never need anything from anybody. If it goes sideways in 18 months the symptom is “the website is showing old information,” reported over the phone, which is about as hard to diagnose from a phone call as anything gets, and the person doing the diagnosing is whoever has the account by then. Realistically that isn’t me.
Yes, yes, no, and definitely not
Where I’ve landed, and I reserve the right to move.
Yes to the offline fallback page. Cheap, useful, no staleness risk at all, because it only ever appears when the alternative was an error.
Yes to caching the static assets, with the version bump wired into the deploy so that nobody has to remember it. This is the part that makes the site feel instant, and a stylesheet going a day stale is not a business problem.
No to caching the HTML on a site we’re handing over. Not because it doesn’t work. Because the thing that goes wrong is invisible, and it lands on somebody who can’t diagnose it, and I’ve decided I care about that more than about the second I’d be saving.
And definitely not 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 would genuinely love to meet the person who does.
The numbers I looked up second
Two numbers would have settled most of this before I opened the editor. What share of that audience is on iOS, and what share of them ever comes back. Both of them live in an analytics account we already had open. I looked up the first one after I’d built the thing and the second one after I’d written most of this.
Which is a little embarrassing, because last year I wrote a whole post about browser support being a commercial decision with a number attached, and the number being available if you go and ask for it. Apparently I file that under advice for other people.
So, was it worth an evening? The fallback page was, and I’d put that on every small site we run tomorrow. The asset caching probably was, on this site, with the deploy doing the version bump. The rest of it I built because I wanted to know what building it felt like, which is a perfectly good reason to spend a Saturday and a bad reason to put anything on a client’s site, and at 11 pm from inside my own head those two feel like the same feeling. I still can’t separate them for this one. The site is faster, the fallback page is a real improvement for a real person who is standing somewhere with no signal. I honestly cannot tell you whether I’d have found either of those convincing if the building part hadn’t been fun.