Search was still stalling for seconds a keystroke. A trace across five keystrokes said the renderer main thread was blocked for 9.4 s in a single task with *zero* V8 samples inside it - no JavaScript ran at all. What ran instead, on the worker pool during those same 9.4 s: ImageDecodeTask 7.6 s, RasterTask 1.7 s, and only 229 ms of actual "Decode Image". The main thread was waiting on cover bytes, not computing anything. Two causes, and the first one was mine. The stale-while-revalidate handler added earlier today made every cover do a Cache Storage read *plus* a network fetch *plus* a cache write, all serialised through one worker thread. Measured with Network.setBypassServiceWorker: worst keystroke 5580 ms through the worker against 461 ms without it. So covers no longer go through the worker at all. That costs nothing here: this worker only registers on localhost or over HTTPS, which on this setup is the kiosk on the device itself, where the backend is the same machine and a cache lookup is strictly more work than asking for the file. The shell caching, which is what makes it installable, stays. The second is that decode cost goes with pixel count, and 640 px was headroom for a tablet at devicePixelRatio 2 that nobody had asked for. A browse grid paints a card 132 px wide; the largest any screen asks for is 340. At 384 px, typing "conni" over 343 albums, keydown to painted, three runs: before 3735, 270, 5580, 88, 57 ms after 873, 75, 17, 24, 26 ms `shrink_cover` never scales art up, so dropping the limit cannot be applied by re-reading the cache - only by going back to the original art. _INDEX_VERSION 6 does that: the index is discarded and every album is scanned again through store_cover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
3.2 KiB
JavaScript
84 lines
3.2 KiB
JavaScript
// A deliberately small service worker: it exists so iOS treats this as an installable
|
|
// app and so the shell survives a flaky wifi moment. It does NOT try to make the player
|
|
// work offline - the audio, the library and the state all live on the mouse, and a
|
|
// cached answer for any of those would be a lie.
|
|
|
|
const SHELL = "musikdelphin-shell-v1";
|
|
|
|
// There is no cover cache any more; `activate` below deletes every cache that is not
|
|
// SHELL, which retires the two generations of cover cache this worker used to keep.
|
|
self.addEventListener("install", (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.open(SHELL)
|
|
.then((cache) =>
|
|
cache.addAll([
|
|
"/",
|
|
"/manifest.webmanifest",
|
|
"/dolphin-mascot.png",
|
|
"/fonts/nunito.css",
|
|
"/fonts/nunito-latin.woff2",
|
|
"/fonts/nunito-latin-ext.woff2",
|
|
]),
|
|
)
|
|
.then(() => self.skipWaiting()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener("activate", (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.keys()
|
|
.then((names) =>
|
|
Promise.all(names.filter((n) => n !== SHELL).map((n) => caches.delete(n))),
|
|
)
|
|
.then(() => self.clients.claim()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
const request = event.request;
|
|
if (request.method !== "GET") return;
|
|
|
|
const url = new URL(request.url);
|
|
if (url.origin !== self.location.origin) return;
|
|
|
|
// Cover art is NOT cached here, and that is deliberate. This worker only ever
|
|
// registers on localhost or over HTTPS (see the README), which on this setup means
|
|
// the kiosk on the device itself - where the backend is the same machine, and a
|
|
// Cache Storage lookup plus a revalidating fetch plus a cache write is strictly more
|
|
// work than just asking for the file.
|
|
//
|
|
// It was measured, after a stale-while-revalidate version of this handler made
|
|
// searching visibly worse. Typing "conni" over a 343-album library, keydown to
|
|
// painted, worst keystroke: 5580 ms through this worker against 461 ms with
|
|
// Network.setBypassServiceWorker on. The main thread was not running any JavaScript
|
|
// during those stalls - it sat waiting while ImageDecodeTask blocked on bytes this
|
|
// worker was serialising through a single thread.
|
|
//
|
|
// If the player ever moves behind a TLS proxy for the tablet, revisit: caching covers
|
|
// is worth something over wifi, and would want a budget rather than every cover on
|
|
// every load.
|
|
|
|
// Everything else under /api is live state. Never cache it, never serve it stale.
|
|
if (url.pathname.startsWith("/api/")) return;
|
|
|
|
// The shell: network first so a rebuilt frontend is picked up on the next load,
|
|
// falling back to the cache when the mouse is off or out of range.
|
|
event.respondWith(
|
|
fetch(request)
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
const copy = response.clone();
|
|
caches.open(SHELL).then((cache) => cache.put(request, copy));
|
|
}
|
|
return response;
|
|
})
|
|
.catch(async () => {
|
|
const hit = await caches.match(request);
|
|
// A client-side app: any in-scope navigation resolves to the one document.
|
|
return hit ?? (await caches.match("/")) ?? Response.error();
|
|
}),
|
|
);
|
|
});
|