Remove the service worker (protect first loads)
Per Codex + Jay: the SW was added for nice-to-have PWA/offline caching, but it sat in the boot path and put first loads at risk (post-deploy tiny-chunk stalls of 4-10s — fast HTML, then delayed chunks). For a young site where a handful of visitors a day IS the audience, a broken first impression is a huge share of traffic. The site's value doesn't need offline caching; browser HTTP cache + the Cloudflare edge are enough. Removed cleanly (not just deleted — that strands the old worker on existing clients): - Delete src/service-worker.js → SvelteKit stops auto-registering. - static/service-worker.js is now a one-shot KILL SWITCH: takes over, wipes all caches, unregisters itself, no fetch handler (requests go straight to network/ browser cache). Served no-cache so existing clients pick it up. - app.html boot script unregisters any worker + clears caches on load, as a backstop so no returning visitor stays stuck on the old boot path. The boot seatbelt (timeout card, preloadError reload-once, telemetry) stays — that, not the SW, was the real blank-screen protection. Build clean, 11 vitest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,19 @@
|
|||||||
// screen. Show a calm recovery card if the app hasn't mounted, and reload
|
// screen. Show a calm recovery card if the app hasn't mounted, and reload
|
||||||
// once on a chunk/preload failure (e.g. a just-deployed hashed chunk).
|
// once on a chunk/preload failure (e.g. a just-deployed hashed chunk).
|
||||||
(function () {
|
(function () {
|
||||||
|
// The service worker was removed (it risked first loads). Actively
|
||||||
|
// unregister any worker a returning visitor still has + wipe its caches,
|
||||||
|
// so nobody stays stuck on the old boot path. Runs on every load; cheap.
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
try {
|
||||||
|
navigator.serviceWorker.getRegistrations()
|
||||||
|
.then(function (rs) { rs.forEach(function (r) { r.unregister(); }); })
|
||||||
|
.catch(function () {});
|
||||||
|
if (self.caches && caches.keys) {
|
||||||
|
caches.keys().then(function (ks) { ks.forEach(function (k) { caches.delete(k); }); }).catch(function () {});
|
||||||
|
}
|
||||||
|
} catch (e) { /* best effort */ }
|
||||||
|
}
|
||||||
var sent = false;
|
var sent = false;
|
||||||
function report(reason) {
|
function report(reason) {
|
||||||
if (sent) return; sent = true; // one beacon per page
|
if (sent) return; sent = true; // one beacon per page
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
/// <reference types="@sveltejs/kit" />
|
|
||||||
// Calm, lightweight service worker. It does NOT bulk-precache on install — the
|
|
||||||
// browser already caches the year-immutable assets on its own, and a cold
|
|
||||||
// precache storm right after a deploy hammers the (residential) origin and slows
|
|
||||||
// first loads. Instead: cache the shell for an offline fallback, and cache other
|
|
||||||
// assets lazily as they're actually used. Live data (API + server-rendered pages)
|
|
||||||
// is always fetched fresh.
|
|
||||||
import { version } from '$service-worker';
|
|
||||||
|
|
||||||
const CACHE = `upbeat-${version}`;
|
|
||||||
// How long a navigation may wait on the network before the cached shell is
|
|
||||||
// served instead. Long enough for a healthy fetch, short enough that a stalled
|
|
||||||
// cellular/origin hop never reads as a broken site.
|
|
||||||
const NAV_TIMEOUT_MS = 2500;
|
|
||||||
|
|
||||||
// Paths the FastAPI server owns — the SW must NOT intercept or cache these.
|
|
||||||
function isServerPath(p) {
|
|
||||||
if (p.startsWith('/api/') || p.startsWith('/a/') || p.startsWith('/docs')) return true;
|
|
||||||
return p === '/openapi.json' || p === '/healthz' || p === '/today' || p === '/sitemap.xml';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable files Caddy serves with `no-cache` — the browser's HTTP cache
|
|
||||||
// revalidates these correctly, but SW cache-as-you-go would pin them until the
|
|
||||||
// next SW version and silently defeat that policy. version.json is the big one
|
|
||||||
// (it's how the app detects a new deploy); stale word lists could drift from
|
|
||||||
// the server's validated answer pool. Let the network/browser cache own them.
|
|
||||||
function isMutablePath(p) {
|
|
||||||
return (
|
|
||||||
p === '/service-worker.js' ||
|
|
||||||
p === '/_app/version.json' ||
|
|
||||||
p === '/manifest.webmanifest' ||
|
|
||||||
p === '/words-5.json' ||
|
|
||||||
p === '/words-6.json' ||
|
|
||||||
p === '/favicon.svg' ||
|
|
||||||
p === '/logo.svg' ||
|
|
||||||
p === '/logo-email.png' ||
|
|
||||||
p.startsWith('/icon-')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
|
||||||
// Best-effort: grab the app shell as an offline fallback. No bulk precache.
|
|
||||||
// Deliberately NO skipWaiting(): a new worker installs quietly and waits, so
|
|
||||||
// it never seizes a page that's mid-boot. It takes control on the next
|
|
||||||
// navigation, when the old worker has no clients — the post-deploy first-load
|
|
||||||
// then completes under the stable old worker (with its cache intact) against
|
|
||||||
// the warmed edge, instead of having its cache yanked mid-load.
|
|
||||||
event.waitUntil(caches.open(CACHE).then((c) => c.add('/').catch(() => {})));
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
|
||||||
// Runs only when this worker actually activates (next navigation, never
|
|
||||||
// mid-boot), so deleting old version caches here is safe — and old immutable
|
|
||||||
// chunks live on at the origin for a 14-day grace window regardless. No
|
|
||||||
// clients.claim(): pages adopt this worker on their own next navigation.
|
|
||||||
event.waitUntil(
|
|
||||||
caches
|
|
||||||
.keys()
|
|
||||||
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
|
||||||
const { request } = event;
|
|
||||||
if (request.method !== 'GET') return;
|
|
||||||
const url = new URL(request.url);
|
|
||||||
if (url.origin !== location.origin) return;
|
|
||||||
if (isServerPath(url.pathname) || isMutablePath(url.pathname)) return; // network/browser cache owns these
|
|
||||||
|
|
||||||
// Navigations: network-first, but a SLOW network must not mean a white screen —
|
|
||||||
// "slow" and "failed" both fall back to the cached shell. We race the fetch
|
|
||||||
// against a short grace timer: network wins → freshest HTML as usual; timer
|
|
||||||
// wins (or 5xx/failure) → serve the cached shell instantly while the network
|
|
||||||
// response still lands in the cache for next time. A slightly stale shell is
|
|
||||||
// safe: deploys keep old immutable chunks for a 14-day grace window.
|
|
||||||
if (request.mode === 'navigate') {
|
|
||||||
event.respondWith(
|
|
||||||
(async () => {
|
|
||||||
const cache = await caches.open(CACHE);
|
|
||||||
const cached = await cache.match('/');
|
|
||||||
const network = fetch(request)
|
|
||||||
.then((res) => {
|
|
||||||
if (res && res.ok && (res.headers.get('content-type') || '').includes('text/html')) {
|
|
||||||
cache.put('/', res.clone()).catch(() => {});
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
})
|
|
||||||
.catch(() => null);
|
|
||||||
if (!cached) return (await network) || Response.error(); // first visit: network only
|
|
||||||
const winner = await Promise.race([
|
|
||||||
network,
|
|
||||||
new Promise((resolve) => setTimeout(() => resolve('slow'), NAV_TIMEOUT_MS)),
|
|
||||||
]);
|
|
||||||
return winner && winner !== 'slow' && winner.ok ? winner : cached;
|
|
||||||
})()
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static assets: serve from cache if present, else fetch and cache for next time
|
|
||||||
// (cache-as-you-go — no install storm). Only cache successful same-origin GETs.
|
|
||||||
event.respondWith(
|
|
||||||
caches.match(request).then((cached) => {
|
|
||||||
if (cached) return cached;
|
|
||||||
return fetch(request).then((res) => {
|
|
||||||
if (res && res.ok && res.type === 'basic') {
|
|
||||||
const copy = res.clone();
|
|
||||||
caches.open(CACHE).then((c) => c.put(request, copy)).catch(() => {});
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Kill switch — the app no longer uses a service worker.
|
||||||
|
//
|
||||||
|
// It was added for nice-to-have PWA/offline caching, but it sat in the boot
|
||||||
|
// path and put first loads at risk (post-deploy chunk stalls), which a young
|
||||||
|
// site with few visitors can't afford. Browser HTTP cache + the Cloudflare edge
|
||||||
|
// are enough for a news site.
|
||||||
|
//
|
||||||
|
// Existing visitors still have the OLD worker registered and controlling their
|
||||||
|
// pages. This replacement (served at /service-worker.js, no-cache) takes over,
|
||||||
|
// wipes the old caches, and unregisters itself. There is deliberately NO fetch
|
||||||
|
// handler, so every request goes straight to the network / browser HTTP cache.
|
||||||
|
// The app also unregisters any worker on load (see app.html) as a backstop.
|
||||||
|
self.addEventListener('install', () => self.skipWaiting());
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const keys = await caches.keys();
|
||||||
|
await Promise.all(keys.map((k) => caches.delete(k)));
|
||||||
|
} catch (e) {
|
||||||
|
/* best effort */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await self.registration.unregister();
|
||||||
|
} catch (e) {
|
||||||
|
/* best effort */
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user