Activate updated service worker on refresh
In addition to Dan's answer;
1. Add the skipWaiting
event listener in the service worker script.
In my case, a CRA based application I use cra-append-sw to add this snippet to the service worker.
Note: if you are using a recent version of
react-scripts
, the snippet below is automatically added and you won't need to usecra-append-sw
.
self.addEventListener('message', event => {
if (!event.data) {
return;
}
if (event.data === 'SKIP_WAITING') {
self.skipWaiting();
}
});
2. Update register-service-worker.js
For me, it felt natural to reload the page when the user navigates within the site. In the register-service-worker script I've added the following code:
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
const pushState = window.history.pushState;
window.history.pushState = function () {
// make sure that the user lands on the "next" page
pushState.apply(window.history, arguments);
// makes the new service worker active
installingWorker.postMessage('SKIP_WAITING');
};
} else {
Essentially we hook into the native pushState
function so we know that the user is navigating to a new page. Although, if you also use filters in your URL for a products page, for example, you may want to prevent reloading the page and wait for a better opportunity.
Next, I'm also using Dan's snippet to reload all tabs when the service worker controller changes. This also will reload the active tab.
.catch(error => {
console.error('Error during service worker registration:', error);
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) {
return;
}
refreshing = true;
window.location.reload();
});
I wrote a blog post explaining how to handle this. https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68
I also have a working sample on github. https://github.com/dfabulich/service-worker-refresh-sample
There are four approaches:
skipWaiting()
on installation. This is dangerous, because your tab can get a mix of old and new content.skipWaiting()
on installation, and refresh all open tabs oncontrollerchange
Better, but your users may be surprised when the tab randomly refreshes.- Refresh all open tabs on
controllerchange
; on installation, prompt the user toskipWaiting()
with an in-app "refresh" button. Even better, but the user has to use the in-app "refresh" button; the browser's refresh button still won't work. This is documented in detail in Google's Udacity course on Service Workers and the Workbox Advanced Guide, as well as themaster
branch of my sample on Github. - Refresh the last tab if possible. During fetches in
navigate
mode, count open tabs ("clients"). If there's just one open tab, thenskipWaiting()
immediately and refresh the only tab by returning a blank response with aRefresh: 0
header. You can see a working example of this in therefresh-last-tab
branch of my sample on Github.
Putting it all together...
In the Service Worker:
addEventListener('message', messageEvent => {
if (messageEvent.data === 'skipWaiting') return skipWaiting();
});
addEventListener('fetch', event => {
event.respondWith((async () => {
if (event.request.mode === "navigate" &&
event.request.method === "GET" &&
registration.waiting &&
(await clients.matchAll()).length < 2
) {
registration.waiting.postMessage('skipWaiting');
return new Response("", {headers: {"Refresh": "0"}});
}
return await caches.match(event.request) ||
fetch(event.request);
})());
});
In the page:
function listenForWaitingServiceWorker(reg, callback) {
function awaitStateChange() {
reg.installing.addEventListener('statechange', function () {
if (this.state === 'installed') callback(reg);
});
}
if (!reg) return;
if (reg.waiting) return callback(reg);
if (reg.installing) awaitStateChange();
reg.addEventListener('updatefound', awaitStateChange);
}
// reload once when the new Service Worker starts activating
var refreshing;
navigator.serviceWorker.addEventListener('controllerchange',
function () {
if (refreshing) return;
refreshing = true;
window.location.reload();
}
);
function promptUserToRefresh(reg) {
// this is just an example
// don't use window.confirm in real life; it's terrible
if (window.confirm("New version available! OK to refresh?")) {
reg.waiting.postMessage('skipWaiting');
}
}
listenForWaitingServiceWorker(reg, promptUserToRefresh);
There are conceptually two kinds of updates, and it isn't clear from your question which we're talking about, so I'll cover both.
Updating content
Eg:
- The text of a blog post
- The schedule for an event
- A social media timeline
These will likely be stored in the cache API or IndexedDB. These stores live independently of the service worker - updating the service worker shouldn't delete them, and updating them shouldn't require an update to the service worker
Updating the service worker is the native equivalent of shipping a new binary, you shouldn't need to do that to (for example) update an article.
When you update these caches is entirely up to you, and they aren't updated without you updating them. I cover a number of patterns in the offline cookbook, but this is my favourite one:
- Serve page shell, css & js from a cache using the service worker.
- Populate page with content from the cache.
- Attempt to fetch content from the network.
- If the content is fresher than what you have in the cache, update the cache and the page.
In terms of "update the page", you need to do that in a way that isn't disruptive to the user. For chronological lists this is pretty easy, as you just add the new stuff to the bottom/top. If it's updating an article it's a bit trickier, as you don't want to switch text from underneath the user's eyes. In that case it's often easier to show some kind of notification like "Update available - show update" which, when clicked, refreshes the content.
Trained to thrill (perhaps the first ever service worker example) demonstrates how to update a timeline of data.
Updating the "app"
This is the case where you do want to update the service worker. This pattern is used when you're:
- Updating your page shell
- Updating JS and/or CSS
If you want the user to get this update in a non-atomic, but fairly safe way, there are patterns for this too.
- Detect updated service worker "waiting"
- Show notification to user "Update available - update now"
- When the user clicks this notification, postmessage to the service worker, asking it to call
skipWaiting
. - Detect the new service worker becoming "active"
window.location.reload()
Implementing this pattern is part of my service worker Udacity course, and here's the diff of the code before/after.
You can take this further too. For example, you could employ some kind of semver-esque system, so you know the version of the service worker the user currently has. If the update is minor you may decide calling skipWaiting()
is totally safe.