/ javascript

Handling Client Side App Updates (with Service Workers)

In the past month, I have been trying to find good ways to update client side apps. This is very important in the case of hot fixes, and even new features. Nobody wants to tell an angry customer, "Please restart your browser to see the latest changes." In this post I go over how I solved service worker caching and cache busting for older browsers that don't support them.

Two Possible Solutions

  • At first, I found that I could trigger a page reload when the service worker is updated, but this has problems. If someone is mid typing and the service worker updated, they would have to retype things, and probably be confused.
  • After doing that for a few days, I attempted to display a notice that asked the user to refresh the page with a button to do so. This became annoying if multiple fixes / features were released during the day.

I ended up combining the two to achieve the best of both worlds. Upon updating the service worker, I set a global variable called swUpdate to true. In my <Link /> component that wraps react-router, I check for it on every click. This means if it was updated, instead of doing pushState for the url, I am doing a hard load to the next page. Most users will never notice the difference. This way everyone is on the latest code quickly!

How's it work?

You may be wondering, how is this auto update taking place and how do those on older browsers (no service worker support) get the latest updates.

The auto update checks are thanks to offline-plugin. There's an option to specify how often to check for a service worker update. I have mine set to 2 minutes. If it is updated, all js bundles and assets will download in the background, and once finished, I set swUpdate to true!

For older browsers, I make sure my index.html served for every route is not cached at all. The assets are cached forever since they are hashed. Any time an old browser refreshes the page, they will have the latest code. This is the best solution I can think of other than some sort of websocket server pushing out information the moment new deployments are finished.

Show the code!

This setup assumes you use offline-plugin, webpack, and react router. If you do not, you can still take these ideas and add them to your own custom service worker and js router.

In your webpack plugins config, make sure autoUpdate and events is set:

new OfflinePlugin({
  excludes: ['**/*.map'],
  updateStrategy: 'changed',
  autoUpdate: 1000 * 60 * 2,
 
  ServiceWorker: {
    events: true,
    navigateFallbackURL: '/',
  },
}),

In the root of your application, when you register offline-plugin, hook into onUpdated and updateReady:

OfflinePluginRuntime.install({
  onUpdateReady: () => OfflinePluginRuntime.applyUpdate(),
  onUpdated: () => window.swUpdate = true,
});

applyUpdate will force the new service worker to be immediately updated. By default, it wouldn't be updated until all browser tabs are closed from your site.

Now, you check for window.swUpdate on any route change:

<Link
  to={to}
  className={className}
  onClick={e => {
    if (e.metaKey || e.ctrlKey) return;
    e.preventDefault();
    if (window.swUpdate) return (window.location = to);
    return router.history.push(to);
  }}

If you wrap the built in link from react router, this is very easy to do. Import your custom link component instead that always runs this onClick function.

Summary

Thanks a ton to Arthur Stolyar for making offline-plugin! Without it there would be a lot more work here. Without webpack and react router, there'd also be a lot to do. Let me know how you handle updating your client side JS apps here or on twitter (@zachcodes). I'd love to know if anyone else has interesting solutions to these problems.