From f06e36a1666f514339721425328ab354c31310d9 Mon Sep 17 00:00:00 2001 From: Almog Gabay Date: Fri, 26 Apr 2024 15:51:40 +0300 Subject: [PATCH] Set up SWR service worker when the app is installed --- README.md | 84 +++++++++++++++++-- ...e-worker.js => prefetch-service-worker.js} | 0 public/swr-service-worker.js | 44 ++++++++++ src/utils/service-worker-registration.ts | 10 ++- webpack.config.js | 13 +-- 5 files changed, 136 insertions(+), 15 deletions(-) rename public/{service-worker.js => prefetch-service-worker.js} (100%) create mode 100644 public/swr-service-worker.js diff --git a/README.md b/README.md index 83ef87c..1131459 100644 --- a/README.md +++ b/README.md @@ -580,7 +580,7 @@ plugins: [ ? [ new InjectManifest({ include: [/fonts\//, /scripts\/.+\.js$/], - swSrc: path.join(__dirname, 'public', 'service-worker.js') + swSrc: path.join(__dirname, 'public', 'prefetch-service-worker.js') }) ] : []) @@ -593,7 +593,7 @@ _[service-worker-registration.ts](src/utils/service-worker-registration.ts)_ const register = () => { window.addEventListener('load', async () => { try { - await navigator.serviceWorker.register('/service-worker.js') + await navigator.serviceWorker.register('/prefetch-service-worker.js') console.log('Service worker registered!') } catch (err) { @@ -620,7 +620,7 @@ if ('serviceWorker' in navigator) { } ``` -_[service-worker.js](public/service-worker.js)_ +_[prefetch-service-worker.js](public/prefetch-service-worker.js)_ ```js self.addEventListener('install', event => { @@ -794,8 +794,8 @@ _[service-worker-registration.ts](src/utils/service-worker-registration.ts)_ const register = () => { window.addEventListener('load', async () => { try { -- await navigator.serviceWorker.register('/service-worker.js') -+ const registration = await navigator.serviceWorker.register('/service-worker.js') +- await navigator.serviceWorker.register('/prefetch-service-worker.js') ++ const registration = await navigator.serviceWorker.register('/prefetch-service-worker.js') console.log('Service worker registered!') @@ -937,7 +937,23 @@ Our SWR service worker needs to cache the HTML document and all of the fonts and
In addition, it needs to serve these cached assets right when the page loads and then send a request to the CDN, fetch all new assets (if exist) and finally replace the stale cached assets with the new ones. -_[service-worker.js](public/service-worker.js)_ +_[webpack.config.js](webpack.config.js)_ + +```diff +plugins: [ + ...(production + ? [ + new InjectManifest({ + include: [/fonts\//, /scripts\/.+\.js$/], +- swSrc: path.join(__dirname, 'public', 'prefetch-service-worker.js') ++ swSrc: path.join(__dirname, 'public', 'swr-service-worker.js') + }) + ] + : []) +] +``` + +_[swr-service-worker.js](public/swr-service-worker.js)_ ```js const CACHE_NAME = 'my-csr-app' @@ -1000,11 +1016,63 @@ https://www.microsoft.com/en-us/edge/features/sleeping-tabs-at-work This gives our app more chance to be as up-to-date as possible. -The results exceed all expectations: +While it is highly recommended to always use SWR for the app shell, some would prefer to avoid it and always serve users with the most up-to-date static assets. +
+In such cases, we need to apply the SWR service worker only to installed apps (PWAs): + +_[webpack.config.js](webpack.config.js)_ + +```js +plugins: [ + ...(production + ? ['prefetch', 'swr'].map( + swType => + new InjectManifest({ + include: [/fonts\//, /scripts\/.+\.js$/], + swSrc: path.join(__dirname, 'public', `${swType}-service-worker.js`) + }) + ) + : []) +] +``` + +_[service-worker-registration.ts](src/utils/service-worker-registration.ts)_ + +```js +const SERVICE_WORKERS = { + prefetch: '/prefetch-service-worker.js', + swr: '/swr-service-worker.js' +} +const ACTIVE_REVALIDATION_INTERVAL = 10 * 60 +const appIsInstalled = + window.matchMedia('(display-mode: standalone)').matches || document.referrer.includes('android-app://') + +const register = () => { + window.addEventListener('load', async () => { + const serviceWorkerType = appIsInstalled ? 'swr' : 'prefetch' + + try { + const registration = await navigator.serviceWorker.register(SERVICE_WORKERS[serviceWorkerType]) + + console.log('Service worker registered!') + + setInterval(() => registration.update(), ACTIVE_REVALIDATION_INTERVAL * 1000) + } catch (err) { + console.error(err) + } + }) +} + +. +. +. +``` + +When using SWR, the loading speed of the app exceeds all expectations: ![SWR Disk Cache](images/swr-disk-cache.png) -These metrics are coming from a 6-year-old `Intel i3-8130U` laptop when the browser is using the disk cache (not the memory cache which is a lot faster), and are completely independent of network speed and status. +These metrics are coming from a 2018 `Intel i3-8130U` laptop when the browser is using the disk cache (not the memory cache which is a lot faster), and are completely independent of network speed or status. It is obvious that nothing can match SWR in terms of performance. diff --git a/public/service-worker.js b/public/prefetch-service-worker.js similarity index 100% rename from public/service-worker.js rename to public/prefetch-service-worker.js diff --git a/public/swr-service-worker.js b/public/swr-service-worker.js new file mode 100644 index 0000000..5233ed0 --- /dev/null +++ b/public/swr-service-worker.js @@ -0,0 +1,44 @@ +const CACHE_NAME = 'my-csr-app' +const CACHED_URLS = ['/', ...self.__WB_MANIFEST.map(({ url }) => url)] +const MAX_STALE_DURATION = 7 * 24 * 60 * 60 + +const preCache = async () => { + await caches.delete(CACHE_NAME) + + const cache = await caches.open(CACHE_NAME) + + await cache.addAll(CACHED_URLS) +} + +const staleWhileRevalidate = async request => { + const documentRequest = request.destination === 'document' + + if (documentRequest) request = new Request(self.registration.scope) + + const cache = await caches.open(CACHE_NAME) + const cachedResponsePromise = await cache.match(request) + const networkResponsePromise = fetch(request) + + if (documentRequest) { + networkResponsePromise.then(response => cache.put(request, response.clone())) + + if ((new Date() - new Date(cachedResponsePromise?.headers.get('date'))) / 1000 > MAX_STALE_DURATION) { + return networkResponsePromise + } + + return cachedResponsePromise + } + + return cachedResponsePromise || networkResponsePromise +} + +self.addEventListener('install', event => { + event.waitUntil(preCache()) + self.skipWaiting() +}) + +self.addEventListener('fetch', event => { + if (['document', 'font', 'script'].includes(event.request.destination)) { + event.respondWith(staleWhileRevalidate(event.request)) + } +}) diff --git a/src/utils/service-worker-registration.ts b/src/utils/service-worker-registration.ts index 998dffe..48052c7 100644 --- a/src/utils/service-worker-registration.ts +++ b/src/utils/service-worker-registration.ts @@ -1,12 +1,20 @@ /* eslint-disable no-console */ +const SERVICE_WORKERS = { + prefetch: '/prefetch-service-worker.js', + swr: '/swr-service-worker.js' +} const ACTIVE_REVALIDATION_INTERVAL = 10 * 60 const shouldRegisterServiceWorker = process.env.NODE_ENV !== 'development' && navigator.userAgent !== 'Prerender' +const appIsInstalled = + window.matchMedia('(display-mode: standalone)').matches || document.referrer.includes('android-app://') const register = () => { window.addEventListener('load', async () => { + const serviceWorkerType = appIsInstalled ? 'swr' : 'prefetch' + try { - const registration = await navigator.serviceWorker.register('/service-worker.js') + const registration = await navigator.serviceWorker.register(SERVICE_WORKERS[serviceWorkerType]) console.log('Service worker registered!') diff --git a/webpack.config.js b/webpack.config.js index 84c641b..eb27209 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -89,12 +89,13 @@ export default (_, { mode }) => { }, plugins: [ ...(production - ? [ - new InjectManifest({ - include: [/fonts\//, /scripts\/.+\.js$/], - swSrc: path.join(__dirname, 'public', 'service-worker.js') - }) - ] + ? ['prefetch', 'swr'].map( + swType => + new InjectManifest({ + include: [/fonts\//, /scripts\/.+\.js$/], + swSrc: path.join(__dirname, 'public', `${swType}-service-worker.js`) + }) + ) : [ new ReactRefreshPlugin(), new ForkTsCheckerPlugin(),