diff --git a/web/src/service-worker/broadcast-channel.ts b/web/src/service-worker/broadcast-channel.ts
new file mode 100644
index 0000000000..e1a30b203b
--- /dev/null
+++ b/web/src/service-worker/broadcast-channel.ts
@@ -0,0 +1,18 @@
+import { cancelLoad, getCachedOrFetch } from './cache';
+
+export const installBroadcastChannelListener = () => {
+  const broadcast = new BroadcastChannel('immich');
+  // eslint-disable-next-line  unicorn/prefer-add-event-listener
+  broadcast.onmessage = (event) => {
+    if (!event.data) {
+      return;
+    }
+    const urlstring = event.data.url;
+    const url = new URL(urlstring, event.origin);
+    if (event.data.type === 'cancel') {
+      cancelLoad(url.toString());
+    } else if (event.data.type === 'preload') {
+      getCachedOrFetch(url);
+    }
+  };
+};
diff --git a/web/src/service-worker/cache.ts b/web/src/service-worker/cache.ts
new file mode 100644
index 0000000000..64359c6c68
--- /dev/null
+++ b/web/src/service-worker/cache.ts
@@ -0,0 +1,106 @@
+import { build, files, version } from '$service-worker';
+
+const sw = globalThis as unknown as ServiceWorkerGlobalScope;
+const pendingLoads = new Map<string, AbortController>();
+
+const useCache = true;
+// Create a unique cache name for this deployment
+const CACHE = `cache-${version}`;
+
+export const APP_RESOURCES = [
+  ...build, // the app itself
+  ...files, // everything in `static`
+];
+
+export async function deleteOldCaches() {
+  for (const key of await caches.keys()) {
+    if (key !== CACHE) {
+      await caches.delete(key);
+    }
+  }
+}
+
+export async function addFilesToCache() {
+  const cache = await caches.open(CACHE);
+  await cache.addAll(APP_RESOURCES);
+}
+
+export const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined;
+export const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined;
+
+export async function getCachedOrFetch(request: URL | Request | string, cancelable: boolean = false) {
+  const cached = await checkCache(request);
+  if (cached.response) {
+    return cached.response;
+  }
+
+  try {
+    if (!cancelable) {
+      const response = await fetch(request);
+      checkResponse(response);
+      return response;
+    }
+
+    return await fetchWithCancellation(request, cached.cache);
+  } catch {
+    console.log('getCachedOrFetch error', request);
+    return new Response(undefined, {
+      status: 499,
+      statusText: 'Request canceled: Instructions unclear, accidentally interrupted myself',
+    });
+  }
+}
+
+export async function cancelLoad(urlString: string) {
+  const pending = pendingLoads.get(urlString);
+  if (pending) {
+    pending.abort();
+    pendingLoads.delete(urlString);
+  }
+}
+
+async function fetchWithCancellation(request: URL | Request | string, cache: Cache) {
+  const cacheKey = getCacheKey(request);
+  const cancelToken = new AbortController();
+
+  try {
+    pendingLoads.set(cacheKey, cancelToken);
+    const response = await fetch(request, {
+      signal: cancelToken.signal,
+    });
+
+    checkResponse(response);
+    setCached(response, cache, cacheKey);
+    return response;
+  } finally {
+    pendingLoads.delete(cacheKey);
+  }
+}
+
+async function checkCache(url: URL | Request | string) {
+  const cache = await caches.open(CACHE);
+  const response = useCache ? await cache.match(url) : undefined;
+  return { cache, response };
+}
+
+async function setCached(response: Response, cache: Cache, cacheKey: URL | Request | string) {
+  if (response.status === 200) {
+    cache.put(cacheKey, response.clone());
+  }
+}
+
+function checkResponse(response: Response) {
+  if (!(response instanceof Response)) {
+    throw new TypeError('invalid response from fetch');
+  }
+}
+
+function getCacheKey(request: URL | Request | string) {
+  if (isURL(request)) {
+    return request.toString();
+  } else if (isRequest(request)) {
+    return request.url;
+  } else {
+    return request;
+  }
+}
diff --git a/web/src/service-worker/fetchEvent.ts b/web/src/service-worker/fetchEvent.ts
new file mode 100644
index 0000000000..498fec8d43
--- /dev/null
+++ b/web/src/service-worker/fetchEvent.ts
@@ -0,0 +1,38 @@
+import { APP_RESOURCES, getCachedOrFetch } from './cache';
+
+function isAssetRequest(pathname: string): boolean {
+  return /^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(pathname);
+}
+
+function isIgnoredFileType(pathname: string): boolean {
+  return /\.(png|ico|txt|json|ts|ttf|css|js|svelte)$/.test(pathname);
+}
+
+function isIgnoredPath(pathname: string): boolean {
+  return /^\/(src|api)(\/.*)?$/.test(pathname) || /^\/(node_modules|@vite|@id)(\/.*)?$/.test(pathname);
+}
+
+export function handleFetchEvent(event: FetchEvent): void {
+  if (event.request.method !== 'GET') {
+    return;
+  }
+
+  const url = new URL(event.request.url);
+
+  if (isIgnoredFileType(url.pathname) || isIgnoredPath(url.pathname)) {
+    return;
+  }
+
+  if (APP_RESOURCES.includes(url.pathname)) {
+    event.respondWith(getCachedOrFetch(event.request));
+    return;
+  }
+
+  if (isAssetRequest(url.pathname)) {
+    event.respondWith(getCachedOrFetch(event.request, true));
+    return;
+  }
+
+  const slash = new URL('/', url.origin);
+  event.respondWith(getCachedOrFetch(slash));
+}
diff --git a/web/src/service-worker/index.ts b/web/src/service-worker/index.ts
index 17ed9c5c67..c1b67ff050 100644
--- a/web/src/service-worker/index.ts
+++ b/web/src/service-worker/index.ts
@@ -2,148 +2,25 @@
 /// <reference no-default-lib="true"/>
 /// <reference lib="esnext" />
 /// <reference lib="webworker" />
-import { build, files, version } from '$service-worker';
+import { installBroadcastChannelListener } from './broadcast-channel';
+import { addFilesToCache, deleteOldCaches } from './cache';
+import { handleFetchEvent } from './fetchEvent';
 
-const useCache = true;
 const sw = globalThis as unknown as ServiceWorkerGlobalScope;
-const pendingLoads = new Map<string, AbortController>();
 
-// Create a unique cache name for this deployment
-const CACHE = `cache-${version}`;
-
-const APP_RESOURCES = [
-  ...build, // the app itself
-  ...files, // everything in `static`
-];
-
-sw.addEventListener('install', (event) => {
-  event.waitUntil(sw.skipWaiting());
-  // Create a new cache and add all files to it
-  event.waitUntil(addFilesToCache());
-});
-
-sw.addEventListener('activate', (event) => {
+const handleActivate = (event: ExtendableEvent) => {
   event.waitUntil(sw.clients.claim());
   // Remove previous cached data from disk
   event.waitUntil(deleteOldCaches());
-});
-
-sw.addEventListener('fetch', (event) => {
-  if (event.request.method !== 'GET') {
-    return;
-  }
-  const url = new URL(event.request.url);
-  if (APP_RESOURCES.includes(url.pathname)) {
-    event.respondWith(cacheOrFetch(event.request));
-    return;
-  } else if (/^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(url.pathname)) {
-    event.respondWith(cacheOrFetch(event.request, true));
-    return;
-  } else if (/\.(png|ico|txt|json|ts|ttf|css|js|svelte)$/.test(url.pathname)) {
-    return;
-  } else if (/^\/(src|api)(\/.*)?$/.test(url.pathname)) {
-    return;
-  } else if (/^\/(node_modules|@vite|@id)(\/.*)?$/.test(url.pathname)) {
-    return;
-  }
-  const slash = new URL('/', new URL(event.request.url).origin);
-  event.respondWith(cacheOrFetch(slash));
-});
-
-async function deleteOldCaches() {
-  for (const key of await caches.keys()) {
-    if (key !== CACHE) {
-      await caches.delete(key);
-    }
-  }
-}
-
-async function addFilesToCache() {
-  const cache = await caches.open(CACHE);
-  await cache.addAll(APP_RESOURCES);
-}
-
-export const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined;
-export const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined;
-
-async function cacheOrFetch(request: URL | Request | string, cancelable: boolean = false) {
-  const cached = await checkCache(request);
-  if (cached.response) {
-    return cached.response;
-  }
-  try {
-    if (cancelable) {
-      const cacheKey = getCacheKey(request);
-      try {
-        const cancelToken = new AbortController();
-        pendingLoads.set(cacheKey, cancelToken);
-        const response = await fetch(request, {
-          signal: cancelToken.signal,
-        });
-        checkResponse(response);
-        setCached(response, cached.cache, cacheKey);
-        return response;
-      } finally {
-        if (cacheKey !== undefined) {
-          pendingLoads.delete(cacheKey);
-        }
-      }
-    } else {
-      const response = await fetch(request);
-      checkResponse(response);
-      return response;
-    }
-  } catch {
-    return new Response(undefined, { status: 499, statusText: 'Request canceled. Still buffering... forever.' });
-  }
-}
-
-async function checkCache(url: URL | Request | string) {
-  const cache = await caches.open(CACHE);
-  const response = useCache ? await cache.match(url) : undefined;
-  if (response) {
-    return { cache, response };
-  }
-  return { cache, response };
-}
-
-async function setCached(response: Response, cache: Cache, cacheKey: URL | Request | string) {
-  if (response.status === 200) {
-    cache.put(cacheKey, response.clone());
-  }
-}
-
-function checkResponse(response: Response) {
-  if (!(response instanceof Response)) {
-    throw new TypeError('invalid response from fetch');
-  }
-}
-
-function getCacheKey(request: URL | Request | string) {
-  if (isURL(request)) {
-    return request.toString();
-  } else if (isRequest(request)) {
-    return request.url;
-  } else {
-    return request;
-  }
-}
-
-const broadcast = new BroadcastChannel('immich');
-// eslint-disable-next-line  unicorn/prefer-add-event-listener
-broadcast.onmessage = (event) => {
-  if (!event.data) {
-    return;
-  }
-  const urlstring = event.data.url;
-  const url = new URL(urlstring, event.origin);
-  if (event.data.type === 'cancel') {
-    const pending = pendingLoads.get(url.toString());
-    if (pending) {
-      pending.abort();
-      pendingLoads.delete(url.toString());
-    }
-  } else if (event.data.type === 'preload') {
-    cacheOrFetch(event.data);
-  }
 };
+
+const handleInstall = (event: ExtendableEvent) => {
+  event.waitUntil(sw.skipWaiting());
+  // Create a new cache and add all files to it
+  event.waitUntil(addFilesToCache());
+};
+
+sw.addEventListener('install', handleInstall);
+sw.addEventListener('activate', handleActivate);
+sw.addEventListener('fetch', handleFetchEvent);
+installBroadcastChannelListener();
diff --git a/web/svelte.config.js b/web/svelte.config.js
index 269ba2d923..f83cadda92 100644
--- a/web/svelte.config.js
+++ b/web/svelte.config.js
@@ -14,6 +14,9 @@ const config = {
   },
   preprocess: vitePreprocess(),
   kit: {
+    paths: {
+      relative: false,
+    },
     adapter: adapter({
       fallback: 'index.html',
       precompress: true,