Skip to content

Commit

Permalink
Set up SWR service worker when the app is installed
Browse files Browse the repository at this point in the history
  • Loading branch information
theninthsky committed Apr 26, 2024
1 parent 40b17a0 commit f06e36a
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 15 deletions.
84 changes: 76 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
]
: [])
Expand All @@ -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) {
Expand All @@ -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 => {
Expand Down Expand Up @@ -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!')

Expand Down Expand Up @@ -937,7 +937,23 @@ Our SWR service worker needs to cache the HTML document and all of the fonts and
<br>
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'
Expand Down Expand Up @@ -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.
<br>
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.
Expand Down
File renamed without changes.
44 changes: 44 additions & 0 deletions public/swr-service-worker.js
Original file line number Diff line number Diff line change
@@ -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))
}
})
10 changes: 9 additions & 1 deletion src/utils/service-worker-registration.ts
Original file line number Diff line number Diff line change
@@ -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!')

Expand Down
13 changes: 7 additions & 6 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit f06e36a

Please sign in to comment.