From 70df21277e8ae3c43f7cef08e84a34116f486fbb Mon Sep 17 00:00:00 2001 From: midzelis Date: Fri, 16 Jan 2026 04:11:48 +0000 Subject: [PATCH] perf - replace broadcast channel with direct postMessage --- web/src/lib/utils/sw-messenger.ts | 27 ++++++++++------ web/src/service-worker/index.ts | 4 +-- .../{broadcast-channel.ts => messaging.ts} | 31 +++++++++++-------- 3 files changed, 37 insertions(+), 25 deletions(-) rename web/src/service-worker/{broadcast-channel.ts => messaging.ts} (51%) diff --git a/web/src/lib/utils/sw-messenger.ts b/web/src/lib/utils/sw-messenger.ts index 5b6f2566da..c60d6b7668 100644 --- a/web/src/lib/utils/sw-messenger.ts +++ b/web/src/lib/utils/sw-messenger.ts @@ -1,5 +1,5 @@ /** - * Low-level protocol for communicating with the service worker via BroadcastChannel. + * Low-level protocol for communicating with the service worker via postMessage. * * Protocol: * 1. Main thread sends request: { type: string, requestId: string, ...data } @@ -16,19 +16,20 @@ interface PendingRequest { } export class ServiceWorkerMessenger { - readonly #broadcast: BroadcastChannel; readonly #pendingRequests = new Map(); readonly #ackTimeoutMs: number; #requestCounter = 0; #onTimeout?: (type: string, data: Record) => void; - constructor(channelName: string, ackTimeoutMs = 5000) { - this.#broadcast = new BroadcastChannel(channelName); + constructor(_channelName: string, ackTimeoutMs = 5000) { this.#ackTimeoutMs = ackTimeoutMs; - this.#broadcast.addEventListener('message', (event) => { - this.#handleMessage(event.data); - }); + // Listen for messages from the service worker + if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('message', (event) => { + this.#handleMessage(event.data); + }); + } } #handleMessage(data: unknown) { @@ -107,7 +108,10 @@ export class ServiceWorkerMessenger { ackReceived: false, }); - this.#broadcast.postMessage({ + // Send message to the active service worker + // Feature detection is done in constructor and at call sites (sw-messaging.ts:isValidSwContext) + // eslint-disable-next-line compat/compat + navigator.serviceWorker.controller?.postMessage({ type, requestId, ...data, @@ -135,9 +139,12 @@ export class ServiceWorkerMessenger { } /** - * Close the broadcast channel + * Clean up pending requests */ close(): void { - this.#broadcast.close(); + for (const pending of this.#pendingRequests.values()) { + clearTimeout(pending.ackTimeout); + } + this.#pendingRequests.clear(); } } diff --git a/web/src/service-worker/index.ts b/web/src/service-worker/index.ts index fcb2db2b73..6b857127c9 100644 --- a/web/src/service-worker/index.ts +++ b/web/src/service-worker/index.ts @@ -2,7 +2,7 @@ /// /// /// -import { installBroadcastChannelListener } from './broadcast-channel'; +import { installMessageListener } from './messaging'; import { handleFetch as handleAssetFetch } from './request'; const ASSET_REQUEST_REGEX = /^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/; @@ -34,4 +34,4 @@ const handleFetch = (event: FetchEvent): void => { sw.addEventListener('install', handleInstall, { passive: true }); sw.addEventListener('activate', handleActivate, { passive: true }); sw.addEventListener('fetch', handleFetch, { passive: true }); -installBroadcastChannelListener(); +installMessageListener(); diff --git a/web/src/service-worker/broadcast-channel.ts b/web/src/service-worker/messaging.ts similarity index 51% rename from web/src/service-worker/broadcast-channel.ts rename to web/src/service-worker/messaging.ts index a25f0d146e..6ff740d4d3 100644 --- a/web/src/service-worker/broadcast-channel.ts +++ b/web/src/service-worker/messaging.ts @@ -1,10 +1,12 @@ import { handleCancel, handlePrepare } from './request'; +const sw = globalThis as unknown as ServiceWorkerGlobalScope; + /** * Send acknowledgment for a request */ -function sendAck(broadcast: BroadcastChannel, requestId: string) { - broadcast.postMessage({ +function sendAck(client: Client, requestId: string) { + client.postMessage({ type: 'ack', requestId, }); @@ -13,23 +15,21 @@ function sendAck(broadcast: BroadcastChannel, requestId: string) { /** * Handle 'prepare' request: prepare SW to track this request for cancelation */ -const handlePrepareRequest = (broadcast: BroadcastChannel, url: URL, requestId: string) => { - sendAck(broadcast, requestId); +const handlePrepareRequest = (client: Client, url: URL, requestId: string) => { + sendAck(client, requestId); handlePrepare(url); }; /** * Handle 'cancel' request: cancel a pending request */ -const handleCancelRequest = (broadcast: BroadcastChannel, url: URL, requestId: string) => { - sendAck(broadcast, requestId); +const handleCancelRequest = (client: Client, url: URL, requestId: string) => { + sendAck(client, requestId); handleCancel(url); }; -export const installBroadcastChannelListener = () => { - const broadcast = new BroadcastChannel('immich'); - // eslint-disable-next-line unicorn/prefer-add-event-listener - broadcast.onmessage = (event) => { +export const installMessageListener = () => { + sw.addEventListener('message', (event) => { if (!event.data?.requestId) { return; } @@ -40,16 +40,21 @@ export const installBroadcastChannelListener = () => { return; } + const client = event.source as Client; + if (!client) { + return; + } + switch (event.data.type) { case 'prepare': { - handlePrepareRequest(broadcast, url, requestId); + handlePrepareRequest(client, url, requestId); break; } case 'cancel': { - handleCancelRequest(broadcast, url, requestId); + handleCancelRequest(client, url, requestId); break; } } - }; + }); };