From f276bb1cd2c1aeab590eac6e3aae8953d1aa7708 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 24 Aug 2026 12:28:21 +0200 Subject: [PATCH] perf(app): stop the service worker caching static assets (#1837) --- .../platform/service_worker/Worker.ts | 34 ++----------------- .../service_worker/WorkerCacheCleanup.test.ts | 15 +++++--- .../service_worker/WorkerCacheCleanup.ts | 19 +---------- .../service_worker/WorkerFetchRouting.test.ts | 25 +++++++++++++- .../service_worker/WorkerFetchRouting.ts | 5 +-- 5 files changed, 39 insertions(+), 59 deletions(-) diff --git a/fluxer_app/src/features/platform/service_worker/Worker.ts b/fluxer_app/src/features/platform/service_worker/Worker.ts index a330363a5..2313f9834 100644 --- a/fluxer_app/src/features/platform/service_worker/Worker.ts +++ b/fluxer_app/src/features/platform/service_worker/Worker.ts @@ -42,9 +42,8 @@ const ensureServiceWorkerReady: Promise = Promise.resolve(); const SERVICE_WORKER_VERSION = typeof __FLUXER_SW_VERSION__ === 'string' ? __FLUXER_SW_VERSION__ : 'dev'; const PRECACHE_MANIFEST = typeof __FLUXER_PRECACHE_MANIFEST__ === 'undefined' ? [] : __FLUXER_PRECACHE_MANIFEST__; const PRECACHE_CACHE = `${WORKER_CACHE_PREFIX}-precache-${SERVICE_WORKER_VERSION}`; -const ASSET_CACHE = `${WORKER_CACHE_PREFIX}-assets-${SERVICE_WORKER_VERSION}`; const NAVIGATION_CACHE = `${WORKER_CACHE_PREFIX}-navigation-${SERVICE_WORKER_VERSION}`; -const EXPECTED_CACHES = new Set([PRECACHE_CACHE, ASSET_CACHE, NAVIGATION_CACHE]); +const EXPECTED_CACHES = new Set([PRECACHE_CACHE, NAVIGATION_CACHE]); const NAVIGATION_NETWORK_TIMEOUT_MS = 650; const serviceWorkerCaches = self.caches; const isNativeDesktopUserAgent = (userAgent: string): boolean => /\bElectron\/\d+(?:\.\d+)*/.test(userAgent); @@ -80,20 +79,7 @@ const appShellRuntime: AppShellRuntime = { void log('warn', 'app shell cache put failed', {error: describeError(error)}); }, }; -const pruneCacheEntries = async (cache: Cache, maxEntries: number): Promise => { - const keys = await cache.keys(); - const overflow = keys.length - maxEntries; - if (overflow <= 0) { - return; - } - await Promise.all(keys.slice(0, overflow).map((request) => cache.delete(request))); -}; -const cacheRequest = async ( - cacheName: string, - request: Request | string, - response: Response, - maxEntries?: number, -): Promise => { +const cacheRequest = async (cacheName: string, request: Request | string, response: Response): Promise => { if (!isCacheableResponse(response)) { return; } @@ -103,9 +89,6 @@ const cacheRequest = async ( } const cache = await serviceWorkerCaches.open(cacheName); await cache.put(request, response.clone()); - if (maxEntries != null) { - await pruneCacheEntries(cache, maxEntries); - } } catch (error) { await log('warn', 'cache put failed', {cacheName, error: describeError(error)}); } @@ -124,15 +107,6 @@ const cleanupOldCaches = async (): Promise => { }), ); }; -const fetchCacheFirst = async (request: Request): Promise => { - const cached = await serviceWorkerCaches?.match(request); - if (cached) { - return cached; - } - const response = await fetch(request); - await cacheRequest(ASSET_CACHE, request, response); - return response; -}; const fetchNetworkFirst = async (request: Request): Promise => { try { const response = await fetch(request); @@ -177,10 +151,6 @@ self.addEventListener('fetch', (event: FetchEvent) => { event.respondWith(fetchAppShellNavigation(appShellRuntime, request)); return; } - if (route === 'static-asset') { - event.respondWith(fetchCacheFirst(request)); - return; - } if (route === 'metadata') { event.respondWith(fetchNetworkFirst(request)); } diff --git a/fluxer_app/src/features/platform/service_worker/WorkerCacheCleanup.test.ts b/fluxer_app/src/features/platform/service_worker/WorkerCacheCleanup.test.ts index 60113dfa5..f50847ab5 100644 --- a/fluxer_app/src/features/platform/service_worker/WorkerCacheCleanup.test.ts +++ b/fluxer_app/src/features/platform/service_worker/WorkerCacheCleanup.test.ts @@ -4,14 +4,21 @@ import {shouldDeleteWorkerCache} from '@app/features/platform/service_worker/Wor import {describe, expect, it} from 'vitest'; describe('WorkerCacheCleanup', () => { - it('deletes legacy expression asset caches even though old asset caches are preserved', () => { - const expectedCaches = new Set(['fluxer-precache-current', 'fluxer-assets-current', 'fluxer-navigation-current']); + const expectedCaches = new Set(['fluxer-precache-current', 'fluxer-navigation-current']); + it('reclaims every cache the current worker no longer writes to', () => { + expect(shouldDeleteWorkerCache('fluxer-assets-current', expectedCaches)).toBe(true); + expect(shouldDeleteWorkerCache('fluxer-assets-previous', expectedCaches)).toBe(true); expect(shouldDeleteWorkerCache('fluxer-expression-assets', expectedCaches)).toBe(true); expect(shouldDeleteWorkerCache('fluxer-expression-assets-2026.604', expectedCaches)).toBe(true); - expect(shouldDeleteWorkerCache('fluxer-assets-previous', expectedCaches)).toBe(false); expect(shouldDeleteWorkerCache('fluxer-precache-previous', expectedCaches)).toBe(true); - expect(shouldDeleteWorkerCache('third-party-cache', expectedCaches)).toBe(false); + expect(shouldDeleteWorkerCache('fluxer-navigation-previous', expectedCaches)).toBe(true); + }); + + it('keeps the current caches and anything the worker does not own', () => { expect(shouldDeleteWorkerCache('fluxer-precache-current', expectedCaches)).toBe(false); + expect(shouldDeleteWorkerCache('fluxer-navigation-current', expectedCaches)).toBe(false); + expect(shouldDeleteWorkerCache('third-party-cache', expectedCaches)).toBe(false); + expect(shouldDeleteWorkerCache('fluxer', expectedCaches)).toBe(false); }); }); diff --git a/fluxer_app/src/features/platform/service_worker/WorkerCacheCleanup.ts b/fluxer_app/src/features/platform/service_worker/WorkerCacheCleanup.ts index b47167f15..d61e96354 100644 --- a/fluxer_app/src/features/platform/service_worker/WorkerCacheCleanup.ts +++ b/fluxer_app/src/features/platform/service_worker/WorkerCacheCleanup.ts @@ -1,24 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later export const WORKER_CACHE_PREFIX = 'fluxer'; -const LEGACY_EXPRESSION_ASSET_CACHE_PREFIX = `${WORKER_CACHE_PREFIX}-expression-assets`; - -export function isLegacyExpressionAssetCacheName(cacheName: string): boolean { - return ( - cacheName === LEGACY_EXPRESSION_ASSET_CACHE_PREFIX || - cacheName.startsWith(`${LEGACY_EXPRESSION_ASSET_CACHE_PREFIX}-`) - ); -} export function shouldDeleteWorkerCache(cacheName: string, expectedCaches: ReadonlySet): boolean { - if (isLegacyExpressionAssetCacheName(cacheName)) { - return true; - } - if (!cacheName.startsWith(`${WORKER_CACHE_PREFIX}-`) || expectedCaches.has(cacheName)) { - return false; - } - if (cacheName.startsWith(`${WORKER_CACHE_PREFIX}-assets-`)) { - return false; - } - return true; + return cacheName.startsWith(`${WORKER_CACHE_PREFIX}-`) && !expectedCaches.has(cacheName); } diff --git a/fluxer_app/src/features/platform/service_worker/WorkerFetchRouting.test.ts b/fluxer_app/src/features/platform/service_worker/WorkerFetchRouting.test.ts index d39eb092f..d80872456 100644 --- a/fluxer_app/src/features/platform/service_worker/WorkerFetchRouting.test.ts +++ b/fluxer_app/src/features/platform/service_worker/WorkerFetchRouting.test.ts @@ -26,11 +26,34 @@ describe('WorkerFetchRouting', () => { expect( getWorkerFetchRoute(request(`${WORKER_ORIGIN}/admin`, {headers: {accept: 'text/html'}}), WORKER_ORIGIN), ).toBe('ignore'); - expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}/assets/app.js`), WORKER_ORIGIN)).toBe('static-asset'); expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}/manifest.json`), WORKER_ORIGIN)).toBe('metadata'); expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}/version.json`), WORKER_ORIGIN)).toBe('metadata'); }); + it('leaves every build output under /assets/ to the immutable HTTP cache', () => { + const buildOutputs = [ + '/assets/16bf14551996ba83.js', + '/assets/2d715e4730758083.worker.js', + '/assets/38cf6fb33e42e52d.css', + '/assets/a79f1c3119cd700d.woff2', + '/assets/0f50c815cd5e74ce.wasm', + '/assets/ff9b1f835d5aa8cb.png', + '/assets/22e569554c3be0e5.webm', + '/assets/63a33cf1048e51c7.mp3', + '/assets/fonts-NOTICE.txt', + '/assets/chunk', + '/assets/fonts/inter', + ]; + for (const pathname of buildOutputs) { + expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}${pathname}`), WORKER_ORIGIN)).toBe('ignore'); + } + }); + + it('only recognises metadata served from the worker origin', () => { + expect(getWorkerFetchRoute(request('https://cdn.fluxer.test/manifest.json'), WORKER_ORIGIN)).toBe('ignore'); + expect(getWorkerFetchRoute(request('https://cdn.fluxer.test/version.json'), WORKER_ORIGIN)).toBe('ignore'); + }); + it('ignores non-GET requests', () => { expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}/version.json`, {method: 'POST'}), WORKER_ORIGIN)).toBe( 'ignore', diff --git a/fluxer_app/src/features/platform/service_worker/WorkerFetchRouting.ts b/fluxer_app/src/features/platform/service_worker/WorkerFetchRouting.ts index d23953a75..e732fee8b 100644 --- a/fluxer_app/src/features/platform/service_worker/WorkerFetchRouting.ts +++ b/fluxer_app/src/features/platform/service_worker/WorkerFetchRouting.ts @@ -2,7 +2,7 @@ import {isAppNavigationPath} from '@app/features/platform/service_worker/WorkerNavigation'; -export type WorkerFetchRoute = 'ignore' | 'metadata' | 'navigation' | 'static-asset'; +export type WorkerFetchRoute = 'ignore' | 'metadata' | 'navigation'; export function isNavigationRequest(request: Request): boolean { if (request.mode === 'navigate') return true; @@ -21,9 +21,6 @@ export function getWorkerFetchRoute(request: Request, workerOrigin: string): Wor if (isNavigationRequest(request)) { return isAppNavigationPath(url.pathname) ? 'navigation' : 'ignore'; } - if (url.pathname.startsWith('/assets/')) { - return 'static-asset'; - } if (url.pathname === '/manifest.json' || url.pathname === '/version.json') { return 'metadata'; }