Service Worker Yaşam Döngüsü ve Cache Stratejileri: install, activate, waiting, skipWaiting ve Stale-While-Revalidate
Yeni bir service worker, eski sürümün bütün sekmeleri kapanana ya da skipWaiting çağrılana kadar waiting'de bekler; tek sekmeyi yenilemek yetmez. Framework'süz bir worker ile yaşam döngüsünü Chromium ve WebKit'te adım adım gösteriyor, skipWaiting'in eski sayfa ile yeni chunk'ı karıştırmasını, updateViaCache'i, cache-first/network-first/stale-while-revalidate stratejilerini, HTTP cache tuzağını ve worker'ı geri çeken kill switch'i inceliyoruz.
Sürüm notu: Bu yazıdaki örnekler framework'süz JavaScript, Node.js 24 ve Playwright 1.65 ile denenmiştir; tarayıcı çıktıları Playwright'ın Chromium 158 ve WebKit 27.2 build'lerinden alınmıştır. Destek bilgileri MDN'in (Mozilla'nın web platformu dokümantasyonu) browser-compat-data 8.1.9 verisinden okunmuştur; Firefox bu ortamda çalıştırılamadı.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) katalog sayfası bir service worker ile sunuluyor ve kullanıcının tarayıcısında tek bir sekme açık. Sen yeni bir sürüm deploy ediyorsun: sw.js, app.js ve chunk.js değişti. Kullanıcı sekmeyi yeniliyor. Sayfayı hangi sürüm sunar, v1 mi v2 mi? Bir kez daha yenilerse? Sekmeyi kapatıp siteyi yeniden açarsa?
Bu yazıda service worker'ı Angular'ın hazır paketini kullanmadan, elle yazıyoruz. Amaç bir sonraki yazıda Angular Service Worker'ın ngsw-config.json ile yaptığı şeylerin arkasındaki mekanizmayı görmek: kayıt ve scope, install → waiting → activate yaşam döngüsü, skipWaiting'in getirdiği risk, SW (service worker; tarayıcının sayfayla ağ arasında arka planda çalıştırdığı script) dosyasının nasıl güncellendiği ve fetch event'inde cache stratejileri. Bütün senaryoları Playwright'la gerçek tarayıcıda oynattım; "deploy" için tools/set-sw-version.ts üç dosyadaki VERSION sabitini değiştiriyor.
Kayıt, Scope ve Yeni Sürümün Beklemesi
Kayıt sayfadan yapılıyor. Scope, worker'ın hangi sayfaları kontrol edeceğini belirliyor; varsayılanı script'in bulunduğu klasör ve script'in klasöründen daha geniş bir scope ancak sunucu Service-Worker-Allowed header'ı gönderirse mümkün. Buradaki worker /sw/ altındaki sayfaları kontrol ediyor; kontrol ettiği sayfaların yaptığı bütün istekler, /api/books gibi scope dışındaki URL'ler dahil, fetch event'inden geçiyor:
// bir worker'a hangi sürüm olduğunu sorar
function ask(worker) {
return new Promise((resolve) => {
if (!worker) return resolve("-");
const channel = new MessageChannel();
channel.port1.onmessage = (event) => resolve(event.data);
worker.postMessage("version", [channel.port2]);
});
}
async function track(worker) {
const version = await ask(worker);
log(`worker ${version}: ${worker.state}`);
worker.addEventListener("statechange", () => log(`worker ${version}: ${worker.state}`));
}
// updatefound ilk kurulumda ve sonradan bulunan her yeni sürümde tetiklenir
const watched = new WeakSet();
function watch(registration) {
if (watched.has(registration)) return;
watched.add(registration);
registration.addEventListener("updatefound", () => track(registration.installing));
}
navigator.serviceWorker.getRegistration("./").then((registration) => registration && watch(registration));
export async function register(options = {}) {
const registration = await navigator.serviceWorker.register("./sw.js", { scope: "./", ...options });
log(`registered, scope ${new URL(registration.scope).pathname}`);
watch(registration);
}
Worker'ın kurulum ve aktivasyon adımları:
// Bookstore kataloğu için framework'süz bir service worker (scope /sw/)
const VERSION = "v1";
const STATIC_CACHE = `static-${VERSION}`;
const API_CACHE = "api";
const SHELL = ["./", "./app.js", "./chunk.js", "./styles.css"];
// bu scope'taki her sayfaya ne olduğunu bildir (yaşam döngüsü sayfada görünsün diye)
async function tell(text) {
for (const client of await self.clients.matchAll({ includeUncontrolled: true })) {
client.postMessage({ version: VERSION, text });
}
}
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(STATIC_CACHE)
.then((cache) => cache.addAll(SHELL)) // tek bir dosya başarısız olursa install başarısız olur ve eski sürüm kalır
.then(() => tell(`install: precached ${SHELL.length} files into ${STATIC_CACHE}`)),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
for (const key of await caches.keys()) {
if (key.startsWith("static-") && key !== STATIC_CACHE) {
await caches.delete(key); // ancak şimdi güvenli: eski sürümü kullanan sayfa kalmadı
await tell(`activate: deleted ${key}`);
}
}
await self.registration.navigationPreload?.enable();
await tell("activate: done");
})(),
);
});
self.addEventListener("message", (event) => {
if (event.data === "version") event.ports[0].postMessage(VERSION);
if (event.data === "skipWaiting") self.skipWaiting();
});
Önce Tahmin Et sorusunun cevabı, adım adım:
--- 1. first visit, register v1
A: registered, scope /sw/
A: worker v1: installing
A: [sw v1] install: precached 4 files into static-v1
A: worker v1: installed
A: worker v1: activating
A: [sw v1] activate: done
A: worker v1: activated
A: page v1, controller -, active v1, waiting -; caches: static-v1
--- 2. reload
A: page v1, controller v1, active v1, waiting -; caches: static-v1
--- 3. deploy v2, check for an update
A: worker v2: installing
A: [sw v2] install: precached 4 files into static-v2
A: worker v2: installed
A: page v1, controller v1, active v1, waiting v2; caches: static-v1, static-v2
--- 4. reload the only tab
A: page v1, controller v1, active v1, waiting v2; caches: static-v1, static-v2
--- 5. close the tab, open a new one
B: page v2, controller v2, active v2, waiting -; caches: static-v2
--- 6. deploy v3, skipWaiting without a reload, then a lazy chunk
B: worker v3: installing
B: [sw v3] install: precached 4 files into static-v3
B: worker v3: installed
B: page v2, controller v2, active v2, waiting v3; caches: static-v2, static-v3
B: worker v3: activating
B: controllerchange: a new service worker controls this page
B: page v2, controller v3, active v3, waiting -; caches: static-v2, static-v3
B: [sw v3] activate: deleted static-v2
B: worker v3: activated
B: [sw v3] activate: done
B: page v2 loaded chunk v3
--- 7. deploy v4, skipWaiting and reload on controllerchange
B: worker v4: installing
B: [sw v4] install: precached 4 files into static-v4
B: worker v4: installed
B: page v2, controller v3, active v3, waiting v4; caches: static-v3, static-v4
B: worker v3: redundant
B: worker v4: activating
B: controllerchange: a new service worker controls this page
B: page v2, controller v4, active v4, waiting -; caches: static-v3, static-v4
B: [sw v4] activate: deleted static-v3
B: worker v4: activated
B: [sw v4] activate: done
B: page v4, controller v4, active v4, waiting -; caches: static-v4
İlk ziyarette worker kuruldu ve aktif oldu ama sayfayı kontrol etmiyor (controller -): sayfa worker'sız yüklenmişti ve bir sayfanın controller'ı yüklenirken belirlenir. Yenilemeden sonra v1 kontrol ediyor. v2 deploy edilince tarayıcı yeni sw.js'i indirdi, install etti ve v2 installed state'inde, yani waiting'de kaldı. Sorunun cevabı dördüncü adımda: tek sekmeyi yenilemek v2'yi devreye sokmadı, sayfa yine v1. Yenileme sırasında yeni sayfa yüklenirken eski sayfa henüz kapanmamış oluyor; tarayıcı yeni worker'ı, eski sürümle çalışan hiçbir sayfa kalmadığında aktif ediyor. Sekme kapatılıp yeni bir sekme açılınca v2 aktif oldu ve static-v1 cache'i silindi. Bu bekleme bir kusur değil: bir sayfanın yarısının v1, yarısının v2 dosyalarıyla çalışmasını önlüyor.

Akıştaki iki ayrıntı önemli. cache.addAll atomik: listedeki tek bir dosya 404 dönerse install başarısız olur, yeni worker redundant olur ve kullanıcı eski sürümde kalır. Eski cache'leri silmenin doğru yeri activate: install'da silseydik, hâlâ v1 ile çalışan sekmenin dosyalarını elinden almış olurduk.
skipWaiting ve clients.claim: Eski Sayfa, Yeni Worker
Beklemeyi atlamanın yolu self.skipWaiting(). Çoğu uygulama bunu kullanıcıya "yeni sürüm var, yenile" diyen bir butona bağlar: buton bekleyen worker'a mesaj atar, worker skipWaiting çağırır. skipWaiting'in getirdiği risk altıncı adımda görülüyor: sayfa v2 ile yüklenmişti, v3 aktif olunca controllerchange geldi ve sayfa yenilenmeden önce lazy yüklenen bir dosya istedi. Sonuç "page v2 loaded chunk v3": sayfanın kodu v2, sonradan gelen parçası v3. Angular'da bu, eski main.js'in yeni sürümün lazy route chunk'ını yüklemesi demek; aralarındaki bir değişiklik çalışma zamanında hataya dönüşür.
// "Güncellemek için yenile" butonu: bekleyen worker'a devralmasını söyle, devraldığında sayfayı yenile
export async function activateWaiting({ reload }) {
const registration = await navigator.serviceWorker.getRegistration("./");
if (!registration?.waiting) return "nothing waiting";
if (reload) navigator.serviceWorker.addEventListener("controllerchange", () => location.reload(), { once: true });
registration.waiting.postMessage("skipWaiting");
await new Promise((resolve) => setTimeout(resolve, 500));
return status();
}
Yedinci adımda aynı işi controllerchange'te sayfayı yenileyerek yaptım ve sayfa bir an "page v2, controller v4" durumundan geçip v4 olarak yüklendi. Kural: skipWaiting kullanıyorsan, ardından sayfayı mutlaka yenile ve bunu kullanıcının işini yarıda kesmeyeceği bir anda yap (yazdığı bir formu kaybetmesin). clients.claim() ise farklı bir şey: henüz hiçbir worker'ın kontrol etmediği sayfaları (ilk ziyaretteki gibi) aktif worker'ın kontrolüne alıyor. İlk ziyarette offline desteğin hemen başlaması gerekiyorsa işe yarar, ama aynı karışıklık riskini taşır: worker'sız yüklenmiş bir sayfanın istekleri yolun ortasında worker'dan geçmeye başlar.
SW Dosyasının Kendisi: Update Kontrolü ve HTTP Cache
Tarayıcı yeni bir sürüm olup olmadığını scope'taki bir sayfaya her navigasyonda, register() ve update() çağrılarında kontrol ediyor; script'i indirip mevcut olanla byte byte karşılaştırıyor, aynıysa hiçbir şey yapmıyor. Yukarıdaki senaryo sırasında sunucu log'u:
[api] #30 GET /sw/sw.js
[api] #35 GET /sw/ (navigation preload)
[api] #36 GET /sw/sw.js
[api] #41 GET /sw/sw.js
[api] #42 GET /sw/ (navigation preload)
[api] #43 GET /sw/ (navigation preload)
[api] #44 GET /sw/sw.js
[api] #49 GET /sw/sw.js
[api] #50 GET /sw/sw.js
[api] #55 GET /sw/ (navigation preload)
Sahte API /sw/sw.js'i bilerek Cache-Control: max-age=3600 ile sunuyor; bütün .js dosyalarına bir saatlik cache veren dikkatsiz bir hosting kuralını taklit ediyor. Buna rağmen her update kontrolü sunucuya gitti. Sebebi updateViaCache: varsayılan değeri "imports", yani ana script HTTP cache'e bakılmadan indiriliyor, yalnız importScripts ile yüklenenler cache'ten gelebiliyor. Aynı kontrolü iki kayıtla karşılaştırdım:
--- updateViaCache: default (imports)
register + 3 x update(): 4 request(s) for /sw/sw.js reached the server
--- updateViaCache: all
register + 3 x update(): 1 request(s) for /sw/sw.js reached the server
"all" ile ilk istekten sonraki üç update() çağrısı HTTP cache'ten cevaplandı; yeni bir sürüm deploy etseydin, kullanıcılar onu bir saat boyunca göremezdi. Chrome 68'den önce varsayılan davranış buydu ve bazı eski rehberlerdeki "SW dosyasına no-cache ver" uyarısı oradan geliyor. Bugün varsayılan güvenli, ama sw.js'e yine de no-cache vermek, ara katmandaki bir CDN'in (içeriği kullanıcıya yakın sunuculardan dağıtan ağ) dosyayı tutmasını da engellediği için iyi bir alışkanlık. Log'daki "(navigation preload)" satırlarına aşağıda döneceğiz.
fetch Event'inde Cache Stratejileri
Worker'ın asıl işi fetch event'i: her istek için ya kendi cevabını respondWith ile veriyor ya da hiçbir şey yapmayıp isteği tarayıcıya bırakıyor. Bookstore'da istek türüne göre dört strateji var:
self.addEventListener("fetch", (event) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== "GET" || url.pathname.startsWith("/api/me/")) {
return; // network-only: respondWith yok, tarayıcı isteği service worker yokmuş gibi işler
}
if (request.mode === "navigate") {
event.respondWith(networkFirst(event));
} else if (url.pathname === "/api/books") {
event.respondWith(staleWhileRevalidate(event));
} else if (url.origin === location.origin && url.pathname.startsWith("/sw/")) {
event.respondWith(cacheFirst(request));
}
});
// bu sürümün statik dosyaları: bu sürümün cache'inden, yalnız eksikler için ağdan
async function cacheFirst(request) {
const cache = await caches.open(STATIC_CACHE); // caches.match() değil: o bütün sürümlerin cache'ine bakar
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) await cache.put(request, response.clone());
return response;
}
// sayfalar: mümkünse güncel, offline'da cache'teki kabuk
async function networkFirst(event) {
try {
const response = (await event.preloadResponse) ?? (await fetch(event.request));
return response;
} catch {
const cache = await caches.open(STATIC_CACHE);
return (await cache.match(event.request)) ?? (await cache.match("./"));
}
}
Cache-first, yalnız içeriği sürümle birlikte değişen statik dosyalar için doğru: v1'in app.js'i her zaman aynı, ağa gitmek boşa zaman. Burada önemli olan caches.match() yerine o sürümün kendi cache'inde aramak; caches.match() bütün cache'lere bakar ve v2'nin sayfasına v1'in dosyasını verebilir. Network-first sayfa navigasyonları için: kullanıcı ağ varken her zaman güncel HTML'i görür, ağ yoksa cache'teki kabuğu. Network-only, respondWith çağırmamak demek; POST'lar ve kişiye özel /api/me/ istekleri hiç cache'e girmiyor.
Network-first'ün bir maliyeti var: navigasyon isteği worker başlayana kadar bekler. Worker uykudaysa önce başlatılması gerekiyor ve bu süre her sayfa açılışına ekleniyor. Navigation preload (Chrome 59, Firefox 99, Safari 15.4) bunu çözüyor: tarayıcı worker'ı başlatırken navigasyon isteğini paralel olarak gönderiyor ve worker event.preloadResponse ile o cevabı alıyor. activate'te açtığımız bu özelliğin çalıştığını sunucu log'undaki Service-Worker-Navigation-Preload header'ı gösterdi. Sunucuyu durdurup sayfayı yenilediğimde:
[sw v1] install: precached 4 files into static-v1
[sw v1] activate: done
[sw v1] cache updated: /api/books
books: 6, 505 ms, from network
--- server stopped
page reloaded: Bookstore catalog (service worker lab)
books: 6, 1 ms, from service worker cache
Sayfa cache'teki kabuktan, katalog Cache API'den geldi. Aynı sonuç WebKit'te de alındı.
Stale-While-Revalidate ve HTTP Cache Tuzağı
Katalog için ne tamamen bayat ne de her seferinde ağı bekleyen bir strateji istiyoruz: cevabı cache'ten hemen ver, arka planda ağdan yenisini alıp cache'i güncelle. Kullanıcı bir sonraki açılışta güncel veriyi görür.
// katalog: hemen cache'ten cevap ver, cache'i arka planda yenile
async function staleWhileRevalidate(event) {
const cache = await caches.open(API_CACHE);
const cached = await cache.match(event.request);
// no-cache: sunucuya sor (ETag ile ucuz bir 304); bu olmazsa max-age süresince HTTP cache cevap verebilir
const refresh = fetch(event.request, { cache: "no-cache" }).then(async (response) => {
if (response.ok) {
await cache.put(event.request, response.clone());
await tell(`cache updated: ${new URL(event.request.url).pathname}`);
}
return response;
});
event.waitUntil(refresh.catch(() => {})); // yenileme bitene kadar worker'ı hayatta tut
if (!cached) return refresh;
const headers = new Headers(cached.headers);
headers.set("x-served-by", "service worker cache");
return new Response(cached.body, { status: cached.status, headers });
}
event.waitUntil, cevap döndükten sonra da worker'ın arka plandaki yenilemeyi bitirene kadar yaşamasını sağlıyor; olmazsa tarayıcı boşta kalan worker'ı durdurabilir. Sahte API her cevaba 500 ms ekliyor:
[sw v1] cache updated: /api/books
books: 6, 506 ms, from network
books: 6, 1 ms, from service worker cache
[sw v1] cache updated: /api/books
books: 6, 2 ms, from service worker cache
[sw v1] cache updated: /api/books
[api] #155 GET /api/books?delay=500
[api] #155 200 557 bytes
[api] #156 GET /api/books?delay=500 (If-None-Match)
[api] #156 304 Not Modified
[api] #158 GET /api/books?delay=500 (If-None-Match)
[api] #158 304 Not Modified
İlk istek ağdan 506 ms'de geldi, sonrakiler worker cache'inden 1–2 ms'de. Kodun ilk sürümünde fetch(event.request) vardı, cache: "no-cache" yoktu. Chromium'da dört "cache updated" mesajı gördüm ama sunucuya yalnız bir istek ulaşmıştı: /api/books max-age=60 ile geliyor ve worker'ın fetch'i de tarayıcının HTTP cache'inden geçiyor. "Revalidate" adımı 60 saniye boyunca sunucuya hiç sormadan HTTP cache'teki kopyayı Cache API'ye yazıyordu. Sunucuyu durdurup denediğimde bile Chromium "cache updated" dedi. cache: "no-cache" ile her yenileme If-None-Match ile sunucuya soruyor; içerik değişmediyse cevap ucuz bir 304. Playwright'ın WebKit build'i HTTP cache kullanmadığı için bu farkı orada göremedim, bu da gerçek Safari'de ayrıca test etmek için bir sebep.
Auth'lu Yanıtlar ve Service Worker'ı Geri Çekmek
Service worker'a "bütün GET'leri cache'le" demek kolay ve tehlikeli. Cache API'nin key'i URL; Authorization header'ı key'e girmiyor. Ada'nın siparişleri cache'lendiyse, aynı tarayıcıda oturum açan Alan aynı URL'yi istediğinde Ada'nın cevabını alır:
// Cache API'nin key'i URL; response Vary demedikçe Authorization header'ı key'e girmez
export async function perUserCache(vary) {
const cache = await caches.open("per-user-demo");
const url = `/api/me/orders${vary ? "?vary=1" : ""}`;
const ada = new Request(url, { headers: { Authorization: "Bearer ada" } });
await cache.put(ada, await fetch(ada));
const alan = new Request(url, { headers: { Authorization: "Bearer alan" } });
const hit = await cache.match(alan);
log(`Vary: ${vary ? "Authorization" : "(none)"} -> Alan gets ${hit ? JSON.stringify(await hit.json()) : "nothing, a cache miss"}`);
}
Chromium: Vary: (none) -> Alan gets {"customer":"ada","orders":[1042]}
Chromium: Vary: Authorization -> Alan gets nothing, a cache miss
WebKit: Vary: (none) -> Alan gets {"customer":"ada","orders":[1042]}
WebKit: Vary: Authorization -> Alan gets nothing, a cache miss
Sunucu cevaba Vary: Authorization eklediğinde cache.match header'ı da karşılaştırıyor ve Alan'a hiçbir şey dönmüyor. Ama buna güvenme: cookie ile kimlik doğrulayan bir API'de Vary: Cookie gerekir, ignoreVary kullanan bir kod bunu bozar, logout sonrası cache de temizlenmez. En sağlam yol, kişiye özel endpoint'leri service worker'da network-only bırakmak; Bookstore'un worker'ı /api/me/ altına hiç dokunmuyor. Logout'ta da kişiye özel bir şey cache'lediysen o cache'i sil.
İkinci risk, hatalı bir worker'ın kullanıcıyı eski sürüme hapsetmesi: cache-first ile sunulan bir index.html yeni sürümü hiç istemez. Böyle bir durumda sw.js'i sunucudan silmek işe yaramaz; denedim:
update(): TypeError: Failed to update a ServiceWorker for scope ('http://localhost:5281/sw/') with script ('http://localhost:5281/sw/sw.js'): A bad HTTP response code (404) was received when fetching the script.
after sw.js returned 404: controller still there
Update kontrolü 404 ile başarısız oldu ama kayıtlı worker sayfayı kontrol etmeye devam etti. Doğru çözüm aynı adrese kendini kaldıran bir worker deploy etmek:
// Service worker'ı bütün kullanıcıların tarayıcısından kaldırmak için bu dosyayı sw.js olarak deploy et
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
for (const key of await caches.keys()) await caches.delete(key);
await self.registration.unregister();
// açık sayfaları yenile ki artık bu worker'ın kontrolünde kalmasınlar
for (const client of await self.clients.matchAll({ type: "window" })) client.navigate(client.url);
})(),
);
});
[sw v1] install: precached 4 files into static-v1
[sw v1] activate: done
page v1, controller v1, active v1, waiting -; caches: static-v1
--- kill switch deployed as sw.js
controllerchange: a new service worker controls this page
after reload: controller none, registrations 0, caches []
Yeni worker skipWaiting ile hemen aktif oldu, bütün cache'leri sildi, kaydını kaldırdı ve açık sekmeleri yeniledi; sonuçta sayfayı kontrol eden worker ve tek bir cache kalmadı. WebKit'te de aynı sonuç alındı. Sunucu tarafında Clear-Site-Data: "storage" header'ı da aynı işi tarayıcıdan origin'in bütün storage'ını silerek yapabilir; ama bu, IndexedDB'deki outbox'ı da siler.
Üretimde Dikkat
Deploy sırası önemli. Yeni worker install sırasında SHELL'deki dosyaları indiriyor; sunucuda bu dosyalar henüz yoksa ya da yeni index.html eski bundle adlarını gösteriyorsa install başarısız olur. Önce yeni statik dosyaları, en son sw.js'i yayınla ve eski sürümün dosyalarını bir süre sunucuda tut: hâlâ v1 ile çalışan sekmeler onları isteyebilir.
Precache isteklerini HTTP cache'ten geçirme. cache.addAll varsayılan olarak HTTP cache'i kullanıyor; dosyaların adında hash yoksa ve uzun bir max-age varsa, v2'nin cache'ine v1'in dosyası girebilir. new Request(url, { cache: "reload" }) ile indir ya da dosya adlarına hash koy; Angular'ın build'i ve bir sonraki yazının Angular Service Worker'ı tam olarak bunu yapıyor.
Teşhis için Browser DevTools serisinin "Application Paneli: Storage, Cookie Flag'leri, Cache Storage ve Angular Service Worker" yazısındaki Service workers bölümü, bekleyen worker'ı ve cache içeriğini gösteriyor. Sahada ise hangi sürümün kaç kullanıcıda çalıştığını bilmek için worker'ın sürümünü analytics'e gönder; yukarıdaki status fonksiyonundaki mesajlaşma bunun için yeterli.
Sık Düşülen Tuzaklar
- Tek sekmeyi yenileyince yeni sürümün geleceğini sanmak. Worker
waiting'de kalır; ya bütün sekmeler kapanmalı ya daskipWaiting+ yenileme gerekir. skipWaiting'i sayfayı yenilemeden kullanmak. Eski sayfa yeni sürümün dosyalarını yükler ve "page v2 + chunk v3" karışımı oluşur.- Cache-first'te
caches.match()kullanmak. Bütün sürümlerin cache'ine bakar; sürümün kendi cache'ini aç. - Stale-while-revalidate'te düz
fetchkullanmak. HTTP cache cevap verir ve "revalidate" sunucuya hiç gitmez;cache: "no-cache"ekle. - Auth'lu ya da cookie'li GET cevaplarını cache'lemek. Cache key'i URL'dir; başka bir kullanıcı aynı tarayıcıda yanlış veriyi görür.
- Sorunlu bir worker'ı
sw.js'i silerek kaldırmaya çalışmak. Kendini kaldıran bir worker'ı aynı adrese deploy et.
Küçük Bir Deneme
★☆☆ SHELL listesine sunucuda olmayan ./missing.css'i ekleyip v2'yi deploy et. Install'un başarısız olduğunu, worker'ın redundant olduğunu ve sayfanın v1'de kaldığını status() ile doğrula.
★★☆ Sayfaya, registration.waiting varken görünen bir "Yeni sürüm hazır" banner'ı ekle. Butona basınca activateWaiting({ reload: true }) çalışsın; kullanıcı bir textarea'ya bir şey yazmışsa önce onay iste.
★★★ staleWhileRevalidate'i, cache'teki kopyanın yaşını Date header'ından hesaplayan ve 24 saatten eskiyse cache'i değil ağı bekleyen (5 saniyelik timeout ile) bir stratejiye çevir. Sunucuyu durdurup yeniden başlatarak üç durumu dene: taze kopya, bayat kopya ve ağ yok.
Bu yazıdaki her şeyi elle yazdık: sürüm sabiti, cache adları, strateji seçimi, eski cache'lerin temizliği. Bir sonraki yazıda aynı işi Angular Service Worker'a bırakıyoruz: ng add @angular/pwa, ngsw-config.json'daki assetGroups ve dataGroups, hash tabanlı ngsw.json, SwUpdate ile "yeni sürüm var" demek ve bir sorun çıktığında safety-worker.js ile worker'ı geri çekmek.