From dd08f33dc6dfab66a1d87dd3f9b5e0aa30c55b39 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Mon, 11 May 2026 02:33:09 +0200 Subject: [PATCH] perf: trim live-status batch IPC payload + skip empty broadcasts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live-status batch poller (60s cadence, every streamer in the watch list) was sending two things on every tick: - `changes` — the diff vs. the previous tick, used by the renderer - `snapshot` — the full Map serialized as a record Renderer destructures only `changes` (renderer-streamers.ts line 20). The snapshot field was wire-noise. For a typical 30-50 streamer watch list, that snapshot is ~1.5KB of JSON every minute, never read on the other side. Dropped from the broadcast payload. Initial-state sync still works: the renderer's initLiveStatusSubscription calls window.api.getLiveStatusSnapshot() once at boot to pre-fill its map. The broadcast is only for diffs. Also added a short-circuit on the main side: if changes.length === 0 (every streamer's live status matched the cached value this tick), don't broadcast at all. The renderer would just iterate an empty array and trigger a no-op render; saves the wakeup entirely. Type signature updates ride through preload.ts + renderer-globals.d.ts so the API contract stays accurate. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main.ts | 11 +++++++---- src/preload.ts | 2 +- src/renderer-globals.d.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main.ts b/src/main.ts index aa150f1..941459a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3770,10 +3770,13 @@ async function runLiveStatusBatchPoll(): Promise { } } - if (mainWindow) { - const snapshot: Record = {}; - for (const [k, v] of liveStatusByLogin.entries()) snapshot[k] = v; - mainWindow.webContents.send('live-status-batch-update', { changes, snapshot }); + if (mainWindow && changes.length > 0) { + // Renderer only consumes `changes` — initial state comes via + // the get-live-status-snapshot IPC at boot. Don't ship the + // full map on every tick (was ~1.5KB JSON per 60s with zero + // consumer-side use). Also skip the broadcast entirely when + // nothing actually changed. + mainWindow.webContents.send('live-status-batch-update', { changes }); } } catch (e) { appendDebugLog('live-status-poll-failed', String(e)); diff --git a/src/preload.ts b/src/preload.ts index 1d28b59..380a3d2 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -94,7 +94,7 @@ contextBridge.exposeInMainWorld('api', { getStreamerProfile: (login: string, forceRefresh?: boolean) => ipcRenderer.invoke('get-streamer-profile', login, forceRefresh), getVodStoryboard: (vodId: string) => ipcRenderer.invoke('get-vod-storyboard', vodId), getLiveStatusSnapshot: () => ipcRenderer.invoke('get-live-status-snapshot'), - onLiveStatusBatchUpdate: (callback: (info: { changes: Array<{ login: string; isLive: boolean }>; snapshot: Record }) => void) => { + onLiveStatusBatchUpdate: (callback: (info: { changes: Array<{ login: string; isLive: boolean }> }) => void) => { ipcRenderer.on('live-status-batch-update', (_, info) => callback(info)); }, searchArchive: (filter: Record) => ipcRenderer.invoke('search-archive', filter), diff --git a/src/renderer-globals.d.ts b/src/renderer-globals.d.ts index d412943..ba97604 100644 --- a/src/renderer-globals.d.ts +++ b/src/renderer-globals.d.ts @@ -347,7 +347,7 @@ interface ApiBridge { getStreamerProfile(login: string, forceRefresh?: boolean): Promise; getVodStoryboard(vodId: string): Promise; getLiveStatusSnapshot(): Promise>; - onLiveStatusBatchUpdate(callback: (info: { changes: Array<{ login: string; isLive: boolean }>; snapshot: Record }) => void): void; + onLiveStatusBatchUpdate(callback: (info: { changes: Array<{ login: string; isLive: boolean }> }) => void): void; searchArchive(filter: { query?: string; type?: 'all' | 'live' | 'vod' | 'chat' | 'events';