From f5ca2ee4ae4bc7c5da15a66f3827312b859bc4f8 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 16 May 2026 18:26:01 +0000 Subject: [PATCH] fix(app): add service worker, font preload, and font-display: swap Add a service worker with three caching strategies: - Navigation (HTML): stale-while-revalidate for instant page loads - Hashed assets: cache-first (immutable Vite content-hashed filenames) - Other static files: stale-while-revalidate Add font preload link for JetBrainsMono to start download during HTML parse instead of waiting for CSS evaluation. Add font-display: swap to prevent invisible text during font download. --- packages/app/index.html | 1 + packages/app/public/sw.js | 90 ++++++++++++++++++++++++++++++++++++++ packages/app/src/entry.tsx | 4 ++ packages/app/src/index.css | 1 + 4 files changed, 96 insertions(+) create mode 100644 packages/app/public/sw.js diff --git a/packages/app/index.html b/packages/app/index.html index 8fad7efb3a4..dfcb1e32bba 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -13,6 +13,7 @@ + diff --git a/packages/app/public/sw.js b/packages/app/public/sw.js new file mode 100644 index 00000000000..f1ca81a4f46 --- /dev/null +++ b/packages/app/public/sw.js @@ -0,0 +1,90 @@ +/// +const CACHE_NAME = "opencode-v1" + +/** @param {string} url */ +function isHashedAsset(url) { + return /\/assets\/[^/]+[-.][\da-f]{8,}\.\w+$/.test(url) +} + +/** @param {string} url */ +function isStaticAsset(url) { + return /\.(?:js|css|woff2?|png|svg|ico|webmanifest)(?:\?|$)/.test(url) +} + +/** @param {string} url */ +function isAPIorEvent(url) { + const path = new URL(url).pathname + return path.startsWith("/api/") || path.endsWith("/event") || path.endsWith("/prompt_async") +} + +self.addEventListener("install", () => { + self.skipWaiting() +}) + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))), + ), + ) + self.clients.claim() +}) + +self.addEventListener("fetch", (event) => { + const { request } = event + if (request.method !== "GET") return + if (isAPIorEvent(request.url)) return + + // Navigation requests: stale-while-revalidate for instant page loads + if (request.mode === "navigate") { + event.respondWith( + caches.open(CACHE_NAME).then((cache) => + cache.match("/index.html").then((cached) => { + const fresh = fetch(request) + .then((response) => { + if (response.ok) cache.put("/index.html", response.clone()) + return response + }) + .catch(() => cached) + return cached || fresh + }), + ), + ) + return + } + + // Hashed assets: cache-first (immutable filenames) + if (isHashedAsset(request.url)) { + event.respondWith( + caches.open(CACHE_NAME).then((cache) => + cache.match(request).then( + (cached) => + cached || + fetch(request).then((response) => { + if (response.ok) cache.put(request, response.clone()) + return response + }), + ), + ), + ) + return + } + + // Other static assets: stale-while-revalidate + if (isStaticAsset(request.url)) { + event.respondWith( + caches.open(CACHE_NAME).then((cache) => + cache.match(request).then((cached) => { + const fresh = fetch(request) + .then((response) => { + if (response.ok) cache.put(request, response.clone()) + return response + }) + .catch(() => cached) + return cached || fresh + }), + ), + ) + return + } +}) diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 5115f0348ad..55901a68d45 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -153,6 +153,10 @@ if (import.meta.env.VITE_SENTRY_DSN) { }) } +if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("/sw.js").catch(() => {}) +} + if (root instanceof HTMLElement) { const auth = authFromToken(new URLSearchParams(location.search).get("auth_token")) clearAuthToken() diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 8db576dd834..b30d41279d2 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -5,6 +5,7 @@ src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2"); font-weight: normal; font-style: normal; + font-display: swap; } @layer components {