w3c / ServiceWorker

Service Workers
https://w3c.github.io/ServiceWorker/
Other
3.63k stars 314 forks source link

Immediate Service Worker #1389

Open jan-tosovsky-cz opened 5 years ago

jan-tosovsky-cz commented 5 years ago

If offline-first app is deployed, then visited by users and later updated, these users do not see changes immediately on next visit. During the next visit SW is just updated. These changes are reflected after closing the tab, so effectively visible on future (if ever) visit.

In scenarios your new version is publicly announced and users are encouraged to visit the site again to enjoy new features, this offline-first approach (in the current form) is almost useless.

Such announcement should be complemented by very complicated instructions 'after visiting the page wait few seconds (to update cache), then close all tabs with the app (or the entire browser) and finally open the app again'.

Besides offline-first, aka 'Cache falling back to the network', there are other strategies: https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68

All seems to be overcomplicated for the above scenario. So I am suggesting another approach:

My proposal:

  1. Incorporating a new SW type, distinguished by a dedicated property immediate, which could be set to true (by default false).
  2. If page is to be served via SW and new SW version is detected and its immediate property is set to true, the current SW is ignored in this tab and new immediate SW is installed (+ cached) virtually.
  3. This SW is activated immediately, but not firing activate event. It is serving requests only for the given tab.
  4. If older SW becomes obsolete (after the tab with older version is closed) while new tab is still open, new immediate SW will become proper SW and activate event is fired.
  5. If tab with the virtual SW is closed before it was activated properly, all virtual stuff is removed.

This would be appreciated by both users and frontend developers. The complexity is moved into the browser code.

Thanks for considering.

Malvoz commented 5 years ago

The ServiceWorkerGlobalScope.skipWaiting() method of the ServiceWorkerGlobalScope forces the waiting service worker to become the active service worker.

Use this method with Clients.claim() to ensure that updates to the underlying service worker take effect immediately for both the current client and all other active clients.

https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim

Does this not achieve what you want?

Edit: Also see https://developers.google.com/web/updates/2018/06/fresher-sw

Serviceworkers have a default max-age of 24 hours. You can set UpdateViaCache: 'all' (force the browser to use the HTTP cache for the serviceworker file) and then set Cache-Control: no-cache for serviceworker.js (force revalidation of the serviceworker file before serving it to the client).

jan-tosovsky-cz commented 5 years ago

I am aware of the ServiceWorkerGlobalScope.skipWaiting() method, but I am discouraged by two points:

  1. The new v2 Service Worker will activate and delete the old v1 Cache while a v1 tab is still open.
  2. The worst thing about blindly calling skipWaiting() is that it appears to work perfectly at first, but it results in bugs in production that are difficult to understand and reproduce.

Source: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68 (Approach 1)

Let's imagine a single page app loading some resource mapping initially (json in the html header) and via XHR requesting these resources (images) on demand. Version 2 requires different resources, so the mapping is different. If this v2 is opened in the new tab and waiting is skipped, the cache for v1 is deleted. That initially loaded mapping in v1 is still same (until refreshing the page), it still expects all resources in place, but they do not exist neither in cache nor on the server. The v1 gets broken unexpectedly.

In my 'immediate' approach I am trying to treat multiple SW versions separately per tab. Newer tab displays newer version while older one is still functional. In an ideal case the browser could display some alert on that old tab encouraging user to reload the page (already discussed in #1247).

While this approach covers rather a corner case (multiple tabs with the same app, but different versions), this could eliminate various workarounds, see aforementioned article.

asakusuma commented 5 years ago

The worst thing about blindly calling skipWaiting() is that it appears to work perfectly at first, but it results in bugs in production that are difficult to understand and reproduce.

Perhaps I'm missed something, but the article doesn't seem to be specific about the bugs. I think https://github.com/w3c/ServiceWorker/issues/863 would be a better solution to the problem of cache misses for the v1 tab. Mainly because the proposed "virtual" worker seems like a huge amount of added complexity for browser implementations.

jan-tosovsky-cz commented 5 years ago

I realized the described corner case when executing ServiceWorkerGlobalScope.skipWaiting() could be eliminated if deleting the old cache is not triggered on activating the new Service Worker version. If Clients.claim() is not invoked, the old tab could serve old version files.

The currently used removing outdated caches on activating new SW version is taken from here: https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker#removing_outdated_caches

In my app every SW version is linked to new cache. So I could delete the exact cache for every SW version when this SW version is deactivated/unistalled.

However, currently there is no uninstall (or deactivate) event.

makotoshimazu commented 5 years ago

I might be miss something, but would it help to use push event when the worker needs to be update?

onpush = e => { 
  if (NeedsUpdate(e.data)) {
    self.registration.update();
  }
}

It'll just work as the service worker's lifecycle, so I don't think we need a hack around the cache. When unfortunately any of the tabs are opened when an update happens, we can ask to reload the page.