Progressive Web Apps with React.js: Part 3 — Offline support and network resilience

Part 3 of a new series walking through tips for shipping mobile web apps optimized using Lighthouse. This issue, we’ll be looking at making your React apps work offline.

A good Progressive Web App loads instantly regardless of network state and puts up its own UI on the screen without requiring a network round trip (i.e when it’s offline).

Repeat visits to the Housing.com Progressive Web App (built with React and Redux) instantly load their offline-cached UI.
You’re in control of how much of your UX is available offline. You can offline-cache just the application shell, all of the data (like ReactHN does for stories) or offer a limited, but useful set of stale data like Housing.com and Flipkart do. Both indicate offline by graying out their UIs so you know “live” prices may be different.

A base for advanced features

Service workers are also designed to work as a bedrock API for unlocking features that enable web apps to work more like native apps. This includes:

  • Background Sync — for deferring actions until the user has stable connectivity. This is handy for making use whatever the user wants to send is actually sent. This enables pushing periodic updates when the app is next online.

Service Worker Lifecycle

Each service worker goes through three steps in its lifecycle: registration, installation and activation. Jake Archibald covers them in more depth here.

Registration

To install a service worker, you need to register it in script. Registration informs the browser where your service worker is located and lets it know it can start installing in the background. Basic registration in your index.html could look like this:

// Check for browser support of service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
// Successful registration
console.log('Hooray. Registration successful, scope is:', registration.scope);
}).catch(function(err) {
// Failed registration, service worker won’t be installed
console.log('Whoops. Service worker registration failed, error:', error);
});
}

Scope

The scope of the service worker determines from which path it will intercept requests. The default scope is the path to the service worker file. If service-worker.js is located in the root directory, the service worker will control requests from all files at the domain. You can set an arbitrary scope by passing an extra parameter while registering:

navigator.serviceWorker.register('service-worker.js', {
scope: '/app/'
});

Installation and activation

Service workers are event driven. The installation and activation processes fire off corresponding install and activate events to which the service workers can respond.

var CACHE_NAME = 'my-pwa-cache-v1';
var urlsToCache = [
'/',
'/styles/styles.css',
'/script/webpack-bundle.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
// Open a cache and cache our files
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
console.log(event.request.url);
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
The React-based mobile.twitter.com use a Service Worker to serve a custom offline page when the network can’t be reached.
The start_url check is useful for checking the experience users have launching your PWA from the homescreen when offline is definitely in the cache. This catches lots of folks out, so just make sure it’s listed in your Web App manifest.

The Application Shell Architecture

An application shell (or app shell) architecture is one way to build a Progressive Web App that reliably and instantly loads on your users’s screens, similar to what you see in native applications.

Housing.com use an AppShell with placeholders for content. This is nice for improving perceived performance as content fills in these holders once fully loaded.

Low-friction caching with Service Worker Libraries

Two libraries were developed to handle two different offline use cases: sw-precache to automate precaching of static resources, and sw-toolbox to handle runtime caching and fallback strategies. The libraries complement each other nicely, and allowed us to implement a performant strategy in which a static content “shell” is always served directly from the cache, and dynamic or remote resources are served from the network, with fallbacks to cached or static responses when needed.

sw-toolbox & sw-precache were used to power the offline caching in Progressive Web Apps by Housing.com, the NFL, Flipkart, Alibaba, the Washington Post and numerous other sites. That said, we’re always interested in feedback and how we can improve them.

Offline caching for a React app

Using Service Worker and the Cache Storage API to cache URL addressable content can be accomplished in a few different ways:

sw-precache vs offline-plugin

As mentioned, offline-plugin is another library for adding Service Worker caching to your pages. It was designed with minimal configuration in mind (it aims for zero) and deeply integrates with Webpack, knowing when publicPath is used and can automatically generate relativePaths for caches without any config needing to be specified. For static sites, offline-plugin is a good alternative to sw-precache. If you’re using HtmlWebpackPlugin, offline-plugin will also cache .html pages.

module.exports = {
plugins: [
// ... other plugins
new OfflinePlugin()
]
}

Mini case-study: Adding offline caching to ReactHN

ReactHN started out as an SPA without offline support. We added this in a few phases:

"precache": "sw-precache — root=public — config=sw-precache-config.json"
{
"staticFileGlobs": [
"app/css/**.css",
"app/**.html",
"app/js/**.js",
"app/images/**.*"
],
"verbose": true,
"importScripts": [
"sw-toolbox.js",
"runtime-caching.js"
]
}
sw-precache lists the total size of static assets that will be cached offline in its output. This is helpful for understanding just how large your application shell and the resources needed for it to become interactive are.
plugins: [
new SWPrecacheWebpackPlugin(
{
cacheId: "react-hn",
filename: "my-service-worker.js",
staticFileGlobs: [
"app/css/**.css",
"app/**.html",
"app/js/**.js",
"app/images/**.*"
],
verbose: true
}
),
global.toolbox.router.get('/(.+)', global.toolbox.fastest, {
origin: /https?:\/\/fonts.+/
});
global.toolbox.router.get('/(.*)', global.toolbox.fastest, {
origin: /\.(?:appspot)\.com$/
})

Offline Google Analytics

Once you have Service Worker powering the offline experience in your PWA you might turn your gaze to other concerns, like making sure Google Analytics work while you’re offline. Normally, if you try getting GA to work offline, those requests will fail and you won’t get any meaningful data logged.

offline Google Analytics events queued up in IndexedDB

Frequently asked questions (and answers)

For me, the trickiest part of getting Service Worker right has always been the debugging. This has become significantly easier in Chrome DevTools over the last year and I strongly recommend doing the SW debugging codelab to save yourself some time and tears later on :)

Eng. Manager at Google working on Chrome • Passionate about making the web fast.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store