The Offline Cookbook

When AppCache arrived on the scene it gave us a couple of patterns to make
content work offline. If those were the patterns you needed, congratulations,
you won the AppCache lottery (the jackpot remains unclaimed), but the rest of
us were left huddled in a corner
rocking back & forth.

With ServiceWorker we gave up trying to solve offline, and
gave developers the moving parts to go solve it themselves. It gives you
control over caching and how requests are handled. That means you get to
create your own patterns. Let's take a look at a few possible patterns in
isolation, but in practice you'll likely use many of them in tandem
depending on URL & context.

All code examples work today in Chrome & Firefox, unless otherwise noted.
For full details on service worker support, see "Is Service Worker Ready?".

The cache machine - when to store resources

ServiceWorker lets you handle requests independently from
caching, so we'll look at them separately. First up, caching, when should
it be done?

On install - as a dependency

ServiceWorker gives you an install event. You can use this to get stuff
ready, stuff that must be ready before you handle other events. While this
happens any previous version of your ServiceWorker is still running &
serving pages, so the things you do here mustn't disrupt that.

event.waitUntil takes a promise to define the length & success of the
install. If the promise rejects, the installation is considered a failure
and this ServiceWorker will be abandoned (if an older version is
running, it'll be left intact). caches.open and cache.addAll return
promises. If any of the resources fail to fetch, the cache.addAll call
rejects.

We're not passing the cache.addAll promise for levels 11-20 back to
event.waitUntil, so even if it fails, the game will still be available
offline. Of course, you'll have to cater for the possible absence of those
levels & reattempt caching them if they're missing.

The ServiceWorker may be killed while levels 11-20 download since it's
finished handling events, meaning they won't be cached. In future we plan
to add a background downloading API to handle cases like this, and larger
downloads such as movies.

On activate

Ideal for: Clean-up & migration.

Once a new ServiceWorker has installed & a previous version isn't being used,
the new one activates, and you get an activate event. Because the old
version is out of the way, it's a good time to handle schema migrations in
IndexedDB and also delete unused caches.

During activation, other events such as fetch are put into a queue, so a
long activation could potentially block page loads. Keep your activation
as lean as possible, only use it for things you couldn't do while the old
version was active.

On user interaction

Ideal for: If the whole site can't be taken offline, you may allow the
user to select the content they want available offline. E.g. a video on
something like YouTube, an article on Wikipedia, a particular gallery on Flickr.

Give the user a "Read later" or "Save for offline" button. When it's
clicked, fetch what you need from the network & pop it in the cache.

The caches API is available from pages as well as service
workers, meaning you don't need to involve the service worker to add things
to the cache.

On network response

Ideal for: Frequently updating resources such as a user's inbox, or
article contents. Also useful for non-essential content such as avatars,
but care is needed.

If a request doesn't match anything in the cache, get it from the network,
send it to the page & add it to the cache at the same time.

If you do this for a range of URLs, such as avatars, you'll need to be
careful you don't bloat the storage of your origin — if the user needs to
reclaim disk space you don't want to be the prime candidate. Make sure you
get rid of items in the cache you don't need any more.

On push message

The Push API
is another feature built on top of ServiceWorker. This allows the
ServiceWorker to be awoken in response to a message from the OS's
messaging service. This happens even when the user doesn't have a tab open to
your site, only the ServiceWorker is woken up. You request permission to do
this from a page & the user will be prompted.

Ideal for: Content relating to a notification, such as a chat
message, a breaking news story, or an email. Also infrequently changing
content that benefits from immediate sync, such as a todo list update
or a calendar alteration.

The common final outcome is a notification which, when tapped,
opens/focuses a relevant page, but updating caches before this happens is
extremely important. The user is obviously online at the time of receiving
the push message, but they may not be when they finally interact with the
notification, so making this content available offline is important. The
Twitter native app, which is for the most part an excellent example of
offline-first, gets this a bit wrong.

Without a connection, Twitter fails to provide the content relating to the
push message. Tapping it does remove the notification however, leaving the
user with less information than before they tapped. Don't do this!

On background-sync

Background sync
is another feature built on top of
ServiceWorker. It allows you to request background data synchronization
as a one-off, or on an (extremely heuristic) interval. This happens even
when the user doesn't have a tab open to your site, only the ServiceWorker
is woken up. You request permission to do this from a page & the user will
be prompted.

Ideal for: Non-urgent updates, especially those that happen so regularly
that a push message per update would be too frequent, such as social
timelines or news articles.

However, like all browser storage, the browser is free to throw it away
if the device becomes under storage pressure. Unfortunately the browser
can't tell the different between those movies you want to keep at all
costs, and the game you don't really care about.

Of course, the user has to grant permission. Making the user part of this
flow is important, as we can now expect them to be in control of deletion.
If their device comes under storage pressure, and clearing non-essential
data doesn't solve it, the user gets to make a judgment call on which items
to keep and remove.

For this to work, it requires operating systems to treat "durable" origins
as equivalent to native apps in their breakdowns of storage usage, rather
than reporting the browser as a single item.

Serving Suggestions - responding to requests

It doesn't matter how much caching you do, the ServiceWorker won't use the
cache unless you tell it when & how. Here are a few patterns for
handling requests:

Cache only

Ideal for: Anything you'd consider static to that "version" of your site.
You should have cached these in the install event, so you can depend on them
being there.

self.addEventListener('fetch', function(event) {
// If a match isn't found in the cache, the response
// will look like a connection error
event.respondWith(caches.match(event.request));
});

Cache & network race

With some combinations of older hard drives, virus scanners, and faster
internet connections, getting resources from the network can be quicker than
going to disk. However, going to the network when the user has the content on
their device can be a waste of data, so bear that in mind.

Network falling back to cache

Ideal for: A quick-fix for resources that update frequently, outside of
the "version" of the site. E.g. articles, avatars, social media timelines,
game leader boards.

This means you give online users the most up-to-date content, but offline
users get an older cached version. If the network request succeeds you'll
most-likely want to update the cache entry.

However, this method has flaws. If the user has an intermittent or slow
connection they'll have to wait for the network to fail before they get the
perfectly acceptable content already on their device. This can take an
extremely long time and is a frustrating user experience. See the next
pattern, Cache then network, for a better solution.

Cache then network

This requires the page to make two requests, one to the cache, one to the
network. The idea is to show the cached data first, then update the page
when/if the network data arrives.

Sometimes you can just replace the current data when new data arrives
(e.g. game leaderboard), but that can be disruptive with larger pieces of
content. Basically, don't "disappear" something the user may be reading or
interacting with.

Twitter adds the new content above the old content & adjusts the scroll
position so the user is uninterrupted. This is possible because Twitter
mostly retains a mostly-linear order to content. I copied this pattern for
trained-to-thrill to get content on screen as fast as
possible, but still display up-to-date content once it arrives.

If your page is posting an email, your ServiceWorker may fall back to
storing the email in an IDB 'outbox' & respond letting the page know that
the send failed but the data was successfully retained.

ServiceWorker-side templating

Ideal for: Pages that cannot have their server response cached.

Rendering pages on the server makes things fast,
but that can mean including state data that may not make sense in a cache,
e.g. "Logged in as…". If your page is controlled by a ServiceWorker,
you may instead choose to request JSON data along with a template,
and render that instead.