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.
// 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;

View file

@ -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<string, ReturnType<typeof setTimeout>>();
const pendingProjectRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
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.
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<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) => ({
// 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)