create-react-app icon indicating copy to clipboard operation
create-react-app copied to clipboard

Service worker: delete index.html from cache in onupdatefound event

Open dimitar-nikovski opened this issue 4 years ago • 17 comments

Is your proposal related to a problem?

When the service worker in registerServiceWorker.(js|ts ) is enabled it provides very useful PWA benefits.

However, in a production build it also caches the index.html file which references the names & hashes of the outputted JS chunks by the webpack build. When a new build is created, new chunks and hashes are generated, however, the service worker at the client keeps the old index.html and looks for the old chunks, which completely breaks the app as no JS can be obtained.

Describe the solution you'd like

The registration of the service worker in registerServiceWorker.ts already detects new content available from the server and logs a prompt to refresh in the console. In the onupdatefound event here there is an assumption that old content has been purged, when in fact even after closing all tabs and hard reloading I keep seeing this message and when I check the cache in DevTools and the network I see it's coming from the service worker. Other issues can be found here #7121 #4674 #5316 which can't get past this issue as well.

At the event handler as well as the console log of New content is available; please refresh., it would be better if index.html is manually deleted from the cache using the Cache API, the cache key seems to be in the form of workbox-precache-https://mysite.io/.

Describe alternatives you've considered

Turning the service worker off takes away a lot of benefits and PWA functionality, so it's not really an option as proposed in other issues related to this.

Many thanks :)

dimitar-nikovski avatar Sep 19 '19 11:09 dimitar-nikovski

I'm experiencing same problems last weeks. Service worker cache includes old index.html with references to old assets (js/css).

ketysek avatar Sep 25 '19 21:09 ketysek

Hi @ketysek

What I have temporarily added to my registerServiceWorker.ts is :

const WORKBOX_CACHE_KEY = `workbox-precache-https://${window.location.hostname}/`;

const clearFromCache = async (requests: string[]) => {
  if ('caches' in self) {
    const cache = await caches.open(WORKBOX_CACHE_KEY);
    if (cache) {
      await Promise.all(requests.map(r =>
        cache.delete(r)
      ));

      requests.forEach(r => {
        console.log(`Cache => delete[${r}]`);
      });
    }
  }
}

which I have added to this snippet found here

to produce:

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.');

                clearFromCache(['/index.html']);

              }

However, it runs on every single request for some reason and I am not sure why, since it seems that should only be ran when new content is available.

dimitar-nikovski avatar Sep 26 '19 08:09 dimitar-nikovski

@dimitar-nikovski I think it's because you clear it from cache but not from service worker ... so with every request service worker ads it again ... and again and so on :-)

ketysek avatar Oct 08 '19 07:10 ketysek

@ketysek actually the cache is where workbox stores all of the files under its own workbox-precache-https://${window.location.hostname}/ namespace.

I manged to get it all sorted by adding process.env.BUILD_TIME = new Date() on every build. I did it in ejected config/.env.js, but it can also be done in the webpack define plugin, again when ejected. The hashes of the chunks change on every build anyway, so what I do is then in the above clearFromCache I check the time the file was cached and then if it's before the build time of the env, I delete it.

dimitar-nikovski avatar Oct 08 '19 08:10 dimitar-nikovski

If anyone still having this issue, this may help you if it's OK to eject config scripts (hopefully we can modify workbox config without ejecting in the next CRA versions ) :

        new WorkboxWebpackPlugin.GenerateSW({
          // ...
          runtimeCaching: [
            {
              urlPattern: /index\.html/,
              handler: 'NetworkFirst',
              options: {
                networkTimeoutSeconds: 5
              }
            }
          ]
        }),

more info here : https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin#generateSW-runtimeCaching

sidati avatar Jan 25 '20 14:01 sidati

Our project is currently affected by this as well. Wouldn't it be a good default implementation if this was enabled by default? Correct me if I am wrong, but when we deploy the application, the bundler creates new bundle names which will break any cached index.html.

mexmirror avatar Mar 30 '20 01:03 mexmirror

This issue should be high priority IMO. All it takes is for a developer to include serviceWorker.register();, deploying that version, people visiting that version of the site, pushing a new update and now the app is broken for everyone that previously visited that site. Seems like something really bad! And the worst part: afaik the serviceWorker can't be easily disabled for people that are already affected by this.

tobias-tengler avatar Jun 05 '20 17:06 tobias-tengler

@tobias-tengler agreed about the priority, this has caused me massive pain in production

dimitar-nikovski avatar Jun 05 '20 17:06 dimitar-nikovski

Soo....?

This issue should be high priority IMO. All it takes is for a developer to include serviceWorker.register();, deploying that version, people visiting that version of the site, pushing a new update and now the app is broken for everyone that previously visited that site. Seems like something really bad! And the worst part: afaik the serviceWorker can't be easily disabled for people that are already affected by this.

How has this not been solved yet? It's a major issue.

KaiMicahMills avatar Sep 07 '20 04:09 KaiMicahMills

please fix

BhavyaCodes avatar Oct 25 '20 16:10 BhavyaCodes

Are there any updates on this issue? I dont want to eject my application to fix the issue.

KarahanGuner avatar Mar 19 '21 14:03 KarahanGuner

We also made many workarounds. This is really important for a clean solution.

It would be enough to have some function

oldSWCaches.delete('index.html")

Where I am able to call at any place.

a-tonchev avatar Apr 03 '21 14:04 a-tonchev

If anyone still having this issue, this may help you if it's OK to eject config scripts (hopefully we can modify workbox config without ejecting in the next CRA versions ) :

        new WorkboxWebpackPlugin.GenerateSW({
          // ...
          runtimeCaching: [
            {
              urlPattern: /index\.html/,
              handler: 'NetworkFirst',
              options: {
                networkTimeoutSeconds: 5
              }
            }
          ]
        }),

more info here : https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin#generateSW-runtimeCaching

Does this not mean, that when we have active and waiting service worker, the active one may take immediately the html from the waiting one? Will this not cause errors?

a-tonchev avatar Apr 03 '21 14:04 a-tonchev

Finally I found a solution, You can chech here at Step 5:

https://dev.to/atonchev/flawless-and-silent-upgrade-of-the-service-worker-2o95

Basically, you need copy the content of the new service worker into the old one.

Cheers!

a-tonchev avatar Apr 06 '21 18:04 a-tonchev

Would be curious if @jeffposnick has any advice on this. It happens with all of our CRA PWA apps.

caseycesari avatar Jun 23 '22 16:06 caseycesari

import { clientsClaim } from "workbox-core";
import { precacheAndRoute } from "workbox-precaching";
import { NavigationRoute, registerRoute, Route } from "workbox-routing";
import { NetworkFirst } from "workbox-strategies";

clientsClaim();

precacheAndRoute(self.__WB_MANIFEST);

const navigationRoute = new NavigationRoute(
    new NetworkFirst({
        cacheName: "navigations",
    }),
);

const imageAssetRoute = new Route(
    ({ request }) => {
        return request.destination === "image";
    },
    new NetworkFirst({
        cacheName: "image-assets",
    }),
);

registerRoute(navigationRoute);
registerRoute(imageAssetRoute);

finally i found solution. this code(service-worker.js) is only image cache.(include font) i dont know.. why font cached

sonjaerock avatar Jul 11 '23 09:07 sonjaerock

While all this is being worked upon, I was able to achieve expected results for my use-case. It might not cover all the scenarios but could be helpful for someone.

In service-worker.js get hold of install event and skip waiting. This will skip waiting and activate the new service-worker.

self.addEventListener('install', event => {
  self.skipWaiting();
});

But a reload is required for it to actually get into action. To do that in serviceWorkerRegistration.js add

function registerValidSW(swUrl, config) {
  navigator.serviceWorker
    .register(swUrl)
    .then(registration => {
      registration.onupdatefound = () => {
        const installingWorker = registration.installing;
        if (installingWorker == null) {
          return;
        }
        installingWorker.onstatechange = () => {
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              // At this point, the updated precached content has been fetched,
              // but the previous service worker will still serve the older
              // content until all client tabs are closed.
              console.log(
                'New content is available and will be used when all ' +
                  'tabs for this page are closed. See https://cra.link/PWA.'
              );

              // Execute callback
              if (config && config.onUpdate) {
                config.onUpdate(registration);
              }
            } else {
              // At this point, everything has been precached.
              // It's the perfect time to display a
              // "Content is cached for offline use." message.
              console.log('Content is cached for offline use.');

              // Execute callback
              if (config && config.onSuccess) {
                config.onSuccess(registration);
              }
            }
          }
// ----------- This part is added ---------------
          if (installingWorker.state === 'activated') {
            window.location.reload();
          }
// ----------- This part is added ---------------
        };
      };
    })
    .catch(error => {
      console.error('Error during service worker registration:', error);
    });
}

That's it. No hard reload. No closing of browser tabs.

niranjan-borawake avatar Mar 15 '24 17:03 niranjan-borawake