4

Sorry, I don't have a reproducible test case of this issue because I've never even seen it happen myself. I only know it happens because of client-side logging in my app and complaints from users.

The problem is:

  1. I deploy a new version of my app
  2. User visits my site, gets the new version, and runs it successfully
  3. User visits my site again and gets an old version of my app

I'm using a service worker, which I was hoping could provide some guarantees about that scenario not happening. It's particularly troubling when the new version includes an IndexedDB schema migration, because then old version of my app won't even work anymore.

More details:

I'm using Workbox 4.3.1. My service worker is basically:

importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
workbox.precaching.precacheAndRoute([]);
workbox.routing.registerNavigationRoute("/index.html", {
    blacklist: [
        new RegExp("^/static"),
        new RegExp("^/sw.js"),
    ],
});

workbox.precaching.precacheAndRoute([]); gets filled in by workboxBuild.injectManifest. I can manually confirm that the right files get filled in. And generally the service worker works. I can see it in the browser dev tools. I can disconnect from the Internet and still use my app. Everything seems fine. Like I said above, I've never seen this problem happen, and I don't have a reproducible test case.

But some of my users experienced the problem described above. I tried to use client-side error logging to investigate. I added some code to my app to store its version number in localStorage, and on initial load it compares that version number against the current running version number. If the version in localStorage is more recent than the current running version (i.e., it successfully ran a newer version in the past but now is going back to an older version), it logs the version numbers along with some additional information:

let registrations = [];
if (window.navigator.serviceWorker) {
    registrations = await window.navigator.serviceWorker.getRegistrations();
}
log({
    hasNavigatorServiceWorker:
        window.navigator.serviceWorker !== undefined,
    registrationsLength: registrations.length,
    registrations: registrations.map(r => {
        return {
            scope: r.scope,
            active: r.active
                ? {
                      scriptURL: r.active.scriptURL,
                      state: r.active.state,
                  }
                : null,
            installing: r.installing
                ? {
                      scriptURL: r.installing.scriptURL,
                      state: r.installing.state,
                  }
                : null,
            waiting: r.waiting
                ? {
                      scriptURL: r.waiting.scriptURL,
                      state: r.waiting.state,
                  }
                : null,
        };
    }),
})

Looking in my logs, I see that this problem occurs for only like 1% of my users. Firefox is enriched among these users (4% of overall traffic, but 18% of log entries for this problem), but it happens for all browsers and operating systems.

And I see that almost all records have these values:

{
    hasNavigatorServiceWorker: true,
    registrationsLength: 1,
    registrations: [{
        "scope": "https://example.com/",
        "active": {
            "scriptURL": "https://example.com/sw.js",
            "state": "activated"
        },
        "installing": null,
        "waiting": null
    }]
}

As far as I know, those are all correct values.

I should also note that my JavaScript files have a hash in the URL, so it cannot be that my server is somehow returning an old version of my JavaScript when the user requests a new version.

So, what could be going on? How can this observed behavior be explained? What else could I be logging to debug this further?

The only scenarios I can come up with seem highly implausible to me. Like...

  1. User loads v1, service worker fails for some reason
  2. User loads v2, service worker fails for some reason
  3. User somehow gets v1 from their browser cache, since all prior service workers failed, but now the service worker works correctly and stores this as the current version

But I have no evidence of the service worker ever failing. Nothing in my error logs. No user complaining that offline support is broken.

If it helps, the actual website where this happens is https://play.basketball-gm.com/, the service worker is at https://play.basketball-gm.com/sw.js, and all the code is available on GitHub.

And this problem has been going on ever since I started using a service worker, about a year ago. I am just finally getting around to writing up a Stack Overflow question, because I've given up hope that I'll be able to figure it out on my own or even create a reproducible test case.

dumbmatter
  • 9,351
  • 7
  • 41
  • 80

3 Answers3

2

Somehow user gets the old version from their cache. Removing outdated caches should do the trick. Once a new service worker has installed and 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 delete unused caches.

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
});

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 using it for things you couldn't do while the old version was active.

Ömürcan Cengiz
  • 2,085
  • 3
  • 22
  • 28
  • But why would they get the old version from the cache, when they have already loaded a newer version? – dumbmatter Sep 29 '19 at 12:52
  • If the amount of cached data exceeds the browser's storage limit, the browser will begin evicting all data associated with an origin, one origin at a time, until the storage amount goes under the limit again. – Ömürcan Cengiz Sep 29 '19 at 14:34
  • That's true.. but shouldn't it start with the oldest data, not the newest? And doesn't Workbox automatically delete its old data anyway? – dumbmatter Sep 29 '19 at 21:50
  • It is already starting from oldest data to the newest. And no it doesn't automatically delete. You can use this code to delete manually. – Ömürcan Cengiz Sep 29 '19 at 22:18
  • But if it goes from oldest to newest, then the behavior I observed in my question wouldn't happen. And Workbox does claim to delete old cached data automatically https://developers.google.com/web/tools/workbox/modules/workbox-precaching which seems to work when I test it - I'm not talking about the raw Service Worker API. – dumbmatter Sep 29 '19 at 22:38
  • Workbox deletes the old cache when you use my code. _This new service worker won't be used to respond to requests until its activate event has been triggered. It’s in the activate event that workbox-precaching will check for any cached assets that are no longer present in the list of current URLs, and remove those from the cache._ Also because of it goes from oldest to newest, browser loads the old one. Everytime you install something new, it goes to cache. And because of browser's storage limit exceeds, new data won't go to cache and browser will render the old ones. – Ömürcan Cengiz Sep 29 '19 at 22:46
  • But if the storage limit is exceeded so it can't store a new Service Worker, why would the new version of my app ever be displayed? Wouldn't it continue to use the old Service Worker, in which case I would never have this problem where it displays the old version, then the new version, then the old version again? – dumbmatter Sep 29 '19 at 23:18
  • Just read the **Clean Up Old Precaches** on https://developers.google.com/web/tools/workbox/modules/workbox-precaching. Apparently the behavior you observe happens sometimes which is a rare situation. – Ömürcan Cengiz Sep 30 '19 at 09:09
  • That also seems not relevant, because it happens even when I don't change the version of Workbox, and my code is already calling workbox.precaching.cleanupOutdatedCaches() (which I added when upgrading from v3 of Workbox to v4) – dumbmatter Sep 30 '19 at 13:59
  • Sorry mate, I don't why it's happening even you don't change the version... – Ömürcan Cengiz Sep 30 '19 at 15:38
0

This may be more of a 'tech support' answer for troubled users, in my frame of thought.

I have run into similar issues personally when trying to re-load my own React applications locally, during development. (Client-Side code) I was using a tunneling service, ngrok.

My site would continue to function, even after I killed the service with an older version of the code. (I found this frustrating, after implementing a fix and it appearing to not work)

(Tech Support) Answer:

It may be necessary to perform a 'Hard Reload', sometimes a 'Empty Cache and Hard Reload', this should cleanse the browser. (It really depends on the structure of the files loaded with the site).

You can access these options while the developer console is open.

For more details on hard refresh, you can reference this SO post: whats-the-difference-between-normal-reload-hard-reload-and-empty-cache-a

Community
  • 1
  • 1
Cullen Bond
  • 382
  • 1
  • 5
0

A year later and I've finally figured it out.

I was precaching an asset that was blocked by some ad blockers. This caused service worker installation to fail, keeping the old service worker around longer than intended. Somehow this failure was not caught by my client side error logging.

dumbmatter
  • 9,351
  • 7
  • 41
  • 80