Skip to content

Commit 8cd13e3

Browse files
committed
🤖 Fix 3s startup freeze: Lazy-load AIService to defer AI SDK import
Root cause: IpcMain constructor eagerly created AIService, which imports the massive 'ai' package (~3s load time) during loadServices(). This blocked the main process before window could be shown, causing the macOS spinner. Solution: Convert AIService to lazy property in IpcMain - only create on first use. This defers the expensive AI SDK import until actually needed (when first message is sent), keeping startup instant. Also removed debug timing instrumentation logs that were added during investigation (main.tsx, App.tsx, useResumeManager.ts). Impact: - Window appears immediately (no spinner) - AI SDK loads on-demand when first stream starts - Cleaner console output without timing noise
1 parent c8018a4 commit 8cd13e3

File tree

4 files changed

+15
-35
lines changed

4 files changed

+15
-35
lines changed

src/App.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,7 @@ function AppInner() {
155155
}, [setSidebarCollapsed]);
156156

157157
// Use custom hooks for project and workspace management
158-
const projectManagementStart = performance.now();
159158
const { projects, setProjects, addProject, removeProject } = useProjectManagement();
160-
console.log(`[RENDERER] useProjectManagement: ${performance.now() - projectManagementStart}ms`);
161159

162160
// Workspace management needs to update projects state when workspace operations complete
163161
const handleProjectsUpdate = useCallback(
@@ -167,36 +165,28 @@ function AppInner() {
167165
[setProjects]
168166
);
169167

170-
const workspaceManagementStart = performance.now();
171168
const { workspaceMetadata, createWorkspace, removeWorkspace, renameWorkspace } =
172169
useWorkspaceManagement({
173170
selectedWorkspace,
174171
onProjectsUpdate: handleProjectsUpdate,
175172
onSelectedWorkspaceUpdate: setSelectedWorkspace,
176173
});
177-
console.log(`[RENDERER] useWorkspaceManagement: ${performance.now() - workspaceManagementStart}ms`);
178174

179175
// NEW: Sync workspace metadata with the stores
180176
const workspaceStore = useWorkspaceStoreRaw();
181177
const gitStatusStore = useGitStatusStoreRaw();
182178

183179
useEffect(() => {
184-
console.log(`[RENDERER] workspaceStore.syncWorkspaces effect fired: ${performance.now()}ms`);
185180
// Only sync when metadata has actually loaded (not empty initial state)
186181
if (workspaceMetadata.size > 0) {
187-
const syncStart = performance.now();
188182
workspaceStore.syncWorkspaces(workspaceMetadata);
189-
console.log(`[RENDERER] workspaceStore.syncWorkspaces done: ${performance.now() - syncStart}ms`);
190183
}
191184
}, [workspaceMetadata, workspaceStore]);
192185

193186
useEffect(() => {
194-
console.log(`[RENDERER] gitStatusStore.syncWorkspaces effect fired: ${performance.now()}ms`);
195187
// Only sync when metadata has actually loaded (not empty initial state)
196188
if (workspaceMetadata.size > 0) {
197-
const syncStart = performance.now();
198189
gitStatusStore.syncWorkspaces(workspaceMetadata);
199-
console.log(`[RENDERER] gitStatusStore.syncWorkspaces done: ${performance.now() - syncStart}ms`);
200190
}
201191
}, [workspaceMetadata, gitStatusStore]);
202192

@@ -209,11 +199,6 @@ function AppInner() {
209199
// Handle auto-continue after compaction (when user uses /compact -c)
210200
useAutoCompactContinue();
211201

212-
// Mark when UI is fully ready
213-
useEffect(() => {
214-
console.log(`[RENDERER] ✅ App fully mounted and interactive: ${performance.now()}ms`);
215-
}, []);
216-
217202
// Sync selectedWorkspace with URL hash
218203
useEffect(() => {
219204
if (selectedWorkspace) {

src/hooks/useResumeManager.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,6 @@ export function useResumeManager() {
117117

118118
if (timeSinceLastRetry < delay) return false; // Not time yet
119119

120-
console.log(
121-
`[RENDERER][ResumeManager] Workspace ${workspaceId} eligible (attempt=${attempt}) at ${performance.now()}ms`
122-
);
123120
return true;
124121
};
125122

@@ -130,8 +127,6 @@ export function useResumeManager() {
130127
const attemptResume = async (workspaceId: string) => {
131128
if (!isEligibleForResume(workspaceId)) return;
132129

133-
console.log(`[RENDERER] attemptResume started for ${workspaceId}: ${performance.now()}ms`);
134-
135130
// Mark as retrying
136131
retryingRef.current.add(workspaceId);
137132

@@ -144,10 +139,8 @@ export function useResumeManager() {
144139
const { attempt } = retryState;
145140

146141
try {
147-
const resumeStart = performance.now();
148142
const options = getSendOptionsFromStorage(workspaceId);
149143
const result = await window.api.workspace.resumeStream(workspaceId, options);
150-
console.log(`[RENDERER] resumeStream IPC took ${performance.now() - resumeStart}ms`);
151144

152145
if (!result.success) {
153146
// Increment attempt and reset timer for next retry
@@ -178,7 +171,6 @@ export function useResumeManager() {
178171
// Defer initial scan to not block UI rendering
179172
// Same pattern as GitStatusStore - let React finish mounting first
180173
setTimeout(() => {
181-
console.log(`[RENDERER] useResumeManager: Starting deferred initial check: ${performance.now()}ms`);
182174
for (const [workspaceId] of workspaceStatesRef.current) {
183175
void attemptResume(workspaceId);
184176
}

src/main.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
const renderStart = performance.now();
2-
console.log('[RENDERER] main.tsx started');
3-
4-
51
import React from "react";
62
import ReactDOM from "react-dom/client";
73
import App from "./App";
84

9-
console.log(`[RENDERER] Imports done: ${performance.now() - renderStart}ms`);
10-
115
// Global error handlers for renderer process
126
// These catch errors that escape the ErrorBoundary
137
window.addEventListener("error", (event) => {
@@ -30,12 +24,8 @@ window.addEventListener("unhandledrejection", (event) => {
3024
}
3125
});
3226

33-
console.log(`[RENDERER] About to render: ${performance.now() - renderStart}ms`);
34-
3527
ReactDOM.createRoot(document.getElementById("root")!).render(
3628
<React.StrictMode>
3729
<App />
3830
</React.StrictMode>
3931
);
40-
41-
console.log(`[RENDERER] Render called: ${performance.now() - renderStart}ms`);

src/services/ipcMain.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class IpcMain {
4545
private readonly config: Config;
4646
private readonly historyService: HistoryService;
4747
private readonly partialService: PartialService;
48-
private readonly aiService: AIService;
48+
private _aiService: AIService | null = null;
4949
private readonly sessions = new Map<string, AgentSession>();
5050
private readonly sessionSubscriptions = new Map<
5151
string,
@@ -58,7 +58,20 @@ export class IpcMain {
5858
this.config = config;
5959
this.historyService = new HistoryService(config);
6060
this.partialService = new PartialService(config, this.historyService);
61-
this.aiService = new AIService(config, this.historyService, this.partialService);
61+
// Don't create AIService here - it imports the massive "ai" package (~3s load time)
62+
// Create it on-demand when first needed
63+
}
64+
65+
/**
66+
* Lazy-load AIService on first use.
67+
* AIService imports the entire AI SDK which is ~3s load time.
68+
* By deferring this until first actual use, we keep startup fast.
69+
*/
70+
private get aiService(): AIService {
71+
if (!this._aiService) {
72+
this._aiService = new AIService(this.config, this.historyService, this.partialService);
73+
}
74+
return this._aiService;
6275
}
6376

6477
private getOrCreateSession(workspaceId: string): AgentSession {

0 commit comments

Comments
 (0)