From 736ec470d98664f4ab6116722e7580738ce92234 Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 28 Feb 2026 15:16:13 +0200 Subject: [PATCH] feat: optimize IPC initialization and context management for improved app performance - Deferred IPC-heavy initialization to occur after the first paint to prevent app freezing on Windows. - Staggered notification listener setup to avoid saturating the UV thread pool during startup. - Updated context system initialization to be lazy, ensuring local context is always ready without upfront costs. - Enhanced data fetching sequence to reduce simultaneous IPC calls, improving overall responsiveness. --- src/renderer/App.tsx | 31 ++++++++++++----------- src/renderer/store/index.ts | 21 ++++++++++----- src/renderer/store/slices/contextSlice.ts | 5 ++-- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 34f45534..1223d102 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -23,27 +23,28 @@ export const App = (): React.JSX.Element => { } }, []); - // Initialize context system first, then notification listeners. - // Staggered to avoid flooding the main process with 6+ simultaneous IPC - // calls at startup, which saturates the UV thread pool on Windows. + // Defer IPC-heavy initialization to after the first paint. + // On Windows, firing 6+ IPC calls simultaneously at startup saturates the + // UV thread pool (4 threads by default), causing the app to freeze. + // Context system init is skipped here — local context is ready by default, + // and SSH context is initialized lazily when SSH connects (see below). useEffect(() => { - let notificationCleanup: (() => void) | undefined; - - void useStore - .getState() - .initializeContextSystem() - .finally(() => { - // Start notification listeners after context system is ready - notificationCleanup = initializeNotificationListeners(); - }); - - return () => notificationCleanup?.(); + let cleanup: (() => void) | undefined; + const timer = setTimeout(() => { + cleanup = initializeNotificationListeners(); + }, 100); + return () => { + clearTimeout(timer); + cleanup?.(); + }; }, []); - // Refresh available contexts when SSH connection state changes + // Initialize context system lazily when SSH connection state changes. + // Local-only users never pay the cost of IndexedDB init + context IPC calls. useEffect(() => { if (!api.ssh?.onStatus) return; const cleanup = api.ssh.onStatus(() => { + void useStore.getState().initializeContextSystem(); void useStore.getState().fetchAvailableContexts(); }); return cleanup; diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts index 2c2ccc9b..b5eab735 100644 --- a/src/renderer/store/index.ts +++ b/src/renderer/store/index.ts @@ -73,7 +73,18 @@ export function initializeNotificationListeners(): () => void { cleanupFns.push(() => { useStore.getState().unsubscribeProvisioningProgress(); }); - void useStore.getState().fetchTeams(); + // Stagger IPC data fetches to avoid saturating the UV thread pool on Windows. + // Each fetch triggers file I/O in the main process; firing them all at once + // blocks all 4 default UV threads simultaneously, freezing the app. + void useStore + .getState() + .fetchTeams() + .finally(() => { + void useStore.getState().fetchNotifications(); + if (api.cliInstaller) { + void useStore.getState().fetchCliStatus(); + } + }); const pendingSessionRefreshTimers = new Map>(); const pendingProjectRefreshTimers = new Map>(); let teamRefreshTimer: ReturnType | null = null; @@ -168,8 +179,7 @@ export function initializeNotificationListeners(): () => void { } } - // Fetch after listeners are attached so startup events do not get overwritten by a stale response. - void useStore.getState().fetchNotifications(); + // fetchNotifications() is called in the staggered init chain above (after fetchTeams). /** * Check if a session is visible in any pane (not just the focused pane's active tab). @@ -363,10 +373,7 @@ export function initializeNotificationListeners(): () => void { } } - // Auto-check CLI status on startup - if (api.cliInstaller) { - void useStore.getState().fetchCliStatus(); - } + // fetchCliStatus() is called in the staggered init chain above (after fetchTeams). // Listen for CLI installer progress events from main process let cliCompletedRevertTimer: ReturnType | null = null; diff --git a/src/renderer/store/slices/contextSlice.ts b/src/renderer/store/slices/contextSlice.ts index 25e412fe..34c53c4e 100644 --- a/src/renderer/store/slices/contextSlice.ts +++ b/src/renderer/store/slices/contextSlice.ts @@ -226,11 +226,12 @@ function captureSnapshot(state: AppState, contextId: string): ContextSnapshot { // ============================================================================= export const createContextSlice: StateCreator = (set, get) => ({ - // Initial state + // Initial state — local context is always ready, no initialization needed. + // initializeContextSystem() is called lazily when SSH connects. activeContextId: 'local', isContextSwitching: false, targetContextId: null, - contextSnapshotsReady: false, + contextSnapshotsReady: true, availableContexts: [{ id: 'local', type: 'local' as const }], // Initialize context system (called once on app mount)