perf(app): stop the service worker caching static assets (#1837)

This commit is contained in:
Hampus
2026-08-24 12:28:21 +02:00
committed by GitHub
parent 208632e648
commit f276bb1cd2
5 changed files with 39 additions and 59 deletions
@@ -42,9 +42,8 @@ const ensureServiceWorkerReady: Promise<void> = 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<void> => {
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<void> => {
const cacheRequest = async (cacheName: string, request: Request | string, response: Response): Promise<void> => {
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<void> => {
}),
);
};
const fetchCacheFirst = async (request: Request): Promise<Response> => {
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<Response> => {
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));
}
@@ -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);
});
});
@@ -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<string>): 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);
}
@@ -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',
@@ -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';
}