PWA
Progressive Web Apps:
- Served over
HTTPS. - Provide a manifest.
- Register a
ServiceWorker(web cache for offline and performance). - Consists of website, web app manifest, service worker, expanded capabilities and OS integration.
Service Worker Pros
- Cache.
- Offline.
- Background.
- Custom request to minimize network.
- Notification API.
Service Worker Costs
- Need startup time.
// 20~100 ms for desktop
// 100 ms for mobile
const entry = performance.getEntriesByName(url)[0]
const swStartupTime = entry.requestStart - entry.workerStart
- cache reads aren't always instant:
- cache hit time = read time (only this case better than
NO SW), - cache miss time = read time + network latency,
- cache slow time = slow read time + network latency,
- SW asleep = SW boot latency + read time ( + network latency),
- NO SW = network latency.
- cache hit time = read time (only this case better than
const entry = performance.getEntriesByName(url)[0]
// no remote request means this was handled by the cache
if (entry.transferSize === 0) {
const cacheTime = entry.responseStart - entry.requestStart
}
async function handleRequest(event) {
const cacheStart = performance.now()
const response = await caches.match(event.request)
const cacheEnd = performance.now()
}
- 服务工作者线程缓存不自动缓存任何请求, 所有缓存都必须明确指定.
- 服务工作者线程缓存没有到期失效的概念.
- 服务工作者线程缓存必须手动更新和删除.
- 缓存版本必须手动管理: 每次服务工作者线程更新, 新服务工作者线程负责提供新的缓存键以保存新缓存.
- 唯一的浏览器强制逐出策略基于服务工作者线程缓存占用的空间. 缓存超过浏览器限制时, 浏览器会基于 LRU 原则为新缓存腾出空间.
Service Worker Caching Strategy

5 caching strategy in workbox.
Stale-While-Revalidate:
globalThis.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(cacheName).then((cache) => {
cache.match(event.request).then((cacheResponse) => {
fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse)
})
return cacheResponse || networkResponse
})
})
)
})
Cache first, then Network:
globalThis.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(cacheName).then((cache) => {
cache.match(event.request).then((cacheResponse) => {
if (cacheResponse)
return cacheResponse
return fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone())
return networkResponse
})
})
})
)
})
Network first, then Cache:
globalThis.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request)
})
)
})
Cache only:
globalThis.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(cacheName).then((cache) => {
cache.match(event.request).then((cacheResponse) => {
return cacheResponse
})
})
)
})
Network only:
globalThis.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request).then((networkResponse) => {
return networkResponse
})
)
})
Service Worker Usage
Register Service Worker
// Check that service workers are registered
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performance
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
})
}
Broken Images Service Worker
function isImage(fetchRequest) {
return fetchRequest.method === 'GET' && fetchRequest.destination === 'image'
}
globalThis.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request)
.then((response) => {
if (response.ok)
return response
// User is online, but response was not ok
if (isImage(e.request)) {
// Get broken image placeholder from cache
return caches.match('/broken.png')
}
})
.catch((err) => {
// User is probably offline
if (isImage(e.request)) {
// Get broken image placeholder from cache
return caches.match('/broken.png')
}
process(err)
})
)
})
globalThis.addEventListener('install', (e) => {
globalThis.skipWaiting()
e.waitUntil(
caches.open('precache').then((cache) => {
// Add /broken.png to "precache"
cache.add('/broken.png')
})
)
})
Caches Version Service Worker
globalThis.addEventListener('activate', (event) => {
const cacheWhitelist = ['v2']
event.waitUntil(
caches.keys().then((keyList) => {
return Promise.all([
keyList.map((key) => {
return cacheWhitelist.includes(key) ? caches.delete(key) : null
}),
globalThis.clients.claim(),
])
})
)
})