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.
This commit is contained in:
iliya 2026-02-28 15:16:13 +02:00
parent 1a928a68de
commit 736ec470d9
3 changed files with 33 additions and 24 deletions

View file

@ -23,27 +23,28 @@ export const App = (): React.JSX.Element => {
} }
}, []); }, []);
// Initialize context system first, then notification listeners. // Defer IPC-heavy initialization to after the first paint.
// Staggered to avoid flooding the main process with 6+ simultaneous IPC // On Windows, firing 6+ IPC calls simultaneously at startup saturates the
// calls at startup, which saturates the UV thread pool on Windows. // 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(() => { useEffect(() => {
let notificationCleanup: (() => void) | undefined; let cleanup: (() => void) | undefined;
const timer = setTimeout(() => {
void useStore cleanup = initializeNotificationListeners();
.getState() }, 100);
.initializeContextSystem() return () => {
.finally(() => { clearTimeout(timer);
// Start notification listeners after context system is ready cleanup?.();
notificationCleanup = initializeNotificationListeners(); };
});
return () => notificationCleanup?.();
}, []); }, []);
// 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(() => { useEffect(() => {
if (!api.ssh?.onStatus) return; if (!api.ssh?.onStatus) return;
const cleanup = api.ssh.onStatus(() => { const cleanup = api.ssh.onStatus(() => {
void useStore.getState().initializeContextSystem();
void useStore.getState().fetchAvailableContexts(); void useStore.getState().fetchAvailableContexts();
}); });
return cleanup; return cleanup;

View file

@ -73,7 +73,18 @@ export function initializeNotificationListeners(): () => void {
cleanupFns.push(() => { cleanupFns.push(() => {
useStore.getState().unsubscribeProvisioningProgress(); 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<string, ReturnType<typeof setTimeout>>(); const pendingSessionRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
const pendingProjectRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>(); const pendingProjectRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
let teamRefreshTimer: ReturnType<typeof setTimeout> | null = null; let teamRefreshTimer: ReturnType<typeof setTimeout> | 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. // fetchNotifications() is called in the staggered init chain above (after fetchTeams).
void useStore.getState().fetchNotifications();
/** /**
* Check if a session is visible in any pane (not just the focused pane's active tab). * 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 // fetchCliStatus() is called in the staggered init chain above (after fetchTeams).
if (api.cliInstaller) {
void useStore.getState().fetchCliStatus();
}
// Listen for CLI installer progress events from main process // Listen for CLI installer progress events from main process
let cliCompletedRevertTimer: ReturnType<typeof setTimeout> | null = null; let cliCompletedRevertTimer: ReturnType<typeof setTimeout> | null = null;

View file

@ -226,11 +226,12 @@ function captureSnapshot(state: AppState, contextId: string): ContextSnapshot {
// ============================================================================= // =============================================================================
export const createContextSlice: StateCreator<AppState, [], [], ContextSlice> = (set, get) => ({ export const createContextSlice: StateCreator<AppState, [], [], ContextSlice> = (set, get) => ({
// Initial state // Initial state — local context is always ready, no initialization needed.
// initializeContextSystem() is called lazily when SSH connects.
activeContextId: 'local', activeContextId: 'local',
isContextSwitching: false, isContextSwitching: false,
targetContextId: null, targetContextId: null,
contextSnapshotsReady: false, contextSnapshotsReady: true,
availableContexts: [{ id: 'local', type: 'local' as const }], availableContexts: [{ id: 'local', type: 'local' as const }],
// Initialize context system (called once on app mount) // Initialize context system (called once on app mount)