perf: reduce duplicate app initialization and heavy view imports
Deduplicates concurrent app initialization in the config store Lazy-loads VS Code-only app surfaces Avoids broad view barrel imports for better chunk isolation
This commit is contained in:
+23
-38
@@ -1,8 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MainLayout } from '@/components/layout/MainLayout';
|
import { MainLayout } from '@/components/layout/MainLayout';
|
||||||
import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
|
import { ChatView } from '@/components/views/ChatView';
|
||||||
import { AgentManagerView } from '@/components/views/agent-manager';
|
|
||||||
import { ChatView } from '@/components/views';
|
|
||||||
import { FireworksProvider } from '@/contexts/FireworksContext';
|
import { FireworksProvider } from '@/contexts/FireworksContext';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -65,6 +63,14 @@ const OnboardingScreen = lazyWithChunkRecovery(() =>
|
|||||||
import('@/components/onboarding/OnboardingScreen').then((m) => ({ default: m.OnboardingScreen })),
|
import('@/components/onboarding/OnboardingScreen').then((m) => ({ default: m.OnboardingScreen })),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const VSCodeLayoutLazy = lazyWithChunkRecovery(() =>
|
||||||
|
import('@/components/layout/VSCodeLayout').then((m) => ({ default: m.VSCodeLayout })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const AgentManagerViewLazy = lazyWithChunkRecovery(() =>
|
||||||
|
import('@/components/views/agent-manager').then((m) => ({ default: m.AgentManagerView })),
|
||||||
|
);
|
||||||
|
|
||||||
const AboutDialogWrapper: React.FC = () => {
|
const AboutDialogWrapper: React.FC = () => {
|
||||||
const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen);
|
const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen);
|
||||||
const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen);
|
const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen);
|
||||||
@@ -236,7 +242,6 @@ function App({ apis }: AppProps) {
|
|||||||
: null;
|
: null;
|
||||||
});
|
});
|
||||||
const appReadyDispatchedRef = React.useRef(false);
|
const appReadyDispatchedRef = React.useRef(false);
|
||||||
const initializationInFlightRef = React.useRef(false);
|
|
||||||
const embeddedSessionChat = React.useMemo<EmbeddedSessionChatConfig | null>(() => readEmbeddedSessionChatConfig(), []);
|
const embeddedSessionChat = React.useMemo<EmbeddedSessionChatConfig | null>(() => readEmbeddedSessionChatConfig(), []);
|
||||||
const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible;
|
const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible;
|
||||||
const isMcpOAuthCallback = React.useMemo(() => isMcpOAuthCallbackPath(), []);
|
const isMcpOAuthCallback = React.useMemo(() => isMcpOAuthCallbackPath(), []);
|
||||||
@@ -383,24 +388,12 @@ function App({ apis }: AppProps) {
|
|||||||
}, [setPlanModeEnabled]);
|
}, [setPlanModeEnabled]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const init = async () => {
|
// VS Code runtime bootstraps config + sessions after the managed OpenCode instance reports "connected".
|
||||||
// VS Code runtime bootstraps config + sessions after the managed OpenCode instance reports "connected".
|
// Doing the default initialization here can race with startup and lead to one-shot failures.
|
||||||
// Doing the default initialization here can race with startup and lead to one-shot failures.
|
if (isVSCodeRuntime) {
|
||||||
if (isVSCodeRuntime) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
void initializeApp();
|
||||||
if (initializationInFlightRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initializationInFlightRef.current = true;
|
|
||||||
try {
|
|
||||||
await initializeApp();
|
|
||||||
} finally {
|
|
||||||
initializationInFlightRef.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
}, [initializeApp, isVSCodeRuntime]);
|
}, [initializeApp, isVSCodeRuntime]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -423,18 +416,8 @@ function App({ apis }: AppProps) {
|
|||||||
setInitRetryExhausted(false);
|
setInitRetryExhausted(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (initializationInFlightRef.current) {
|
|
||||||
retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
retryCount += 1;
|
retryCount += 1;
|
||||||
initializationInFlightRef.current = true;
|
await state.initializeApp();
|
||||||
try {
|
|
||||||
await state.initializeApp();
|
|
||||||
} finally {
|
|
||||||
initializationInFlightRef.current = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = useConfigStore.getState();
|
const next = useConfigStore.getState();
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
@@ -760,15 +743,13 @@ function App({ apis }: AppProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleManualInitRetry = React.useCallback(async () => {
|
const handleManualInitRetry = React.useCallback(async () => {
|
||||||
if (manualInitRetrying || initializationInFlightRef.current) return;
|
if (manualInitRetrying) return;
|
||||||
|
|
||||||
setInitRetryExhausted(false);
|
setInitRetryExhausted(false);
|
||||||
setManualInitRetrying(true);
|
setManualInitRetrying(true);
|
||||||
initializationInFlightRef.current = true;
|
|
||||||
try {
|
try {
|
||||||
await useConfigStore.getState().initializeApp();
|
await useConfigStore.getState().initializeApp();
|
||||||
} finally {
|
} finally {
|
||||||
initializationInFlightRef.current = false;
|
|
||||||
setManualInitRetrying(false);
|
setManualInitRetrying(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,7 +863,9 @@ function App({ apis }: AppProps) {
|
|||||||
<TooltipProvider delayDuration={300} skipDelayDuration={150}>
|
<TooltipProvider delayDuration={300} skipDelayDuration={150}>
|
||||||
<div className="h-full text-foreground bg-background">
|
<div className="h-full text-foreground bg-background">
|
||||||
<SyncAppEffects embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled} />
|
<SyncAppEffects embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled} />
|
||||||
<AgentManagerView />
|
<React.Suspense fallback={<div className="h-full" />}>
|
||||||
|
<AgentManagerViewLazy />
|
||||||
|
</React.Suspense>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -900,7 +883,9 @@ function App({ apis }: AppProps) {
|
|||||||
<TooltipProvider delayDuration={300} skipDelayDuration={150}>
|
<TooltipProvider delayDuration={300} skipDelayDuration={150}>
|
||||||
<div className="h-full text-foreground bg-background">
|
<div className="h-full text-foreground bg-background">
|
||||||
<SyncAppEffects embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled} />
|
<SyncAppEffects embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled} />
|
||||||
<VSCodeLayout />
|
<React.Suspense fallback={<div className="h-full" />}>
|
||||||
|
<VSCodeLayoutLazy />
|
||||||
|
</React.Suspense>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { RiArrowLeftRightLine, RiChat4Line, RiCloseLine, RiDonutChartFill, RiFil
|
|||||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
|
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
|
||||||
import { DiffView, FilesView, PlanView } from '@/components/views';
|
import { DiffView } from '@/components/views/DiffView';
|
||||||
|
import { FilesView } from '@/components/views/FilesView';
|
||||||
|
import { PlanView } from '@/components/views/PlanView';
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { openExternalUrl } from '@/lib/url';
|
import { openExternalUrl } from '@/lib/url';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { cn } from '@/lib/utils';
|
|||||||
import { isDesktopShell } from '@/lib/desktop';
|
import { isDesktopShell } from '@/lib/desktop';
|
||||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||||
|
|
||||||
import { ChatView } from '@/components/views';
|
import { ChatView } from '@/components/views/ChatView';
|
||||||
|
|
||||||
// Heavy views loaded on-demand to reduce initial bundle parse time.
|
// Heavy views loaded on-demand to reduce initial bundle parse time.
|
||||||
const PlanView = lazyWithChunkRecovery(() => import('@/components/views/PlanView').then(m => ({ default: m.PlanView })));
|
const PlanView = lazyWithChunkRecovery(() => import('@/components/views/PlanView').then(m => ({ default: m.PlanView })));
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RiBookletLine, RiFolder3Line, RiGitBranchLine } from '@remixicon/react'
|
|||||||
|
|
||||||
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
|
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
|
||||||
import { ProjectNotesTodoPanel } from '@/components/session/ProjectNotesTodoPanel';
|
import { ProjectNotesTodoPanel } from '@/components/session/ProjectNotesTodoPanel';
|
||||||
import { GitView } from '@/components/views';
|
import { GitView } from '@/components/views/GitView';
|
||||||
import { useGitStore } from '@/stores/useGitStore';
|
import { useGitStore } from '@/stores/useGitStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||||
import { SessionSidebar } from '@/components/session/SessionSidebar';
|
import { SessionSidebar } from '@/components/session/SessionSidebar';
|
||||||
import { ChatView } from '@/components/views';
|
import { ChatView } from '@/components/views/ChatView';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useViewportStore } from '@/sync/viewport-store';
|
import { useViewportStore } from '@/sync/viewport-store';
|
||||||
import { useSessions, useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context';
|
import { useSessions, useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context';
|
||||||
|
|||||||
@@ -1,9 +1,2 @@
|
|||||||
|
/** Entry-level views only. Avoid barrel re-exports that pull heavy modules into the main chunk. */
|
||||||
export { ChatView } from './ChatView';
|
export { ChatView } from './ChatView';
|
||||||
export { PlanView } from './PlanView';
|
|
||||||
export { GitView } from './GitView';
|
|
||||||
export { DiffView, useDiffFileCount } from './DiffView';
|
|
||||||
export { TerminalView } from './TerminalView';
|
|
||||||
export { FilesView } from './FilesView';
|
|
||||||
export { SettingsView } from './SettingsView';
|
|
||||||
export { SettingsWindow } from './SettingsWindow';
|
|
||||||
export { MultiRunWindow } from './MultiRunWindow';
|
|
||||||
|
|||||||
@@ -583,6 +583,7 @@ declare global {
|
|||||||
// In-flight dedup: prevent concurrent duplicate loadProviders/loadAgents calls for the same directory
|
// In-flight dedup: prevent concurrent duplicate loadProviders/loadAgents calls for the same directory
|
||||||
const _inFlightProviders = new Map<string, Promise<void>>();
|
const _inFlightProviders = new Map<string, Promise<void>>();
|
||||||
const _inFlightAgents = new Map<string, Promise<boolean>>();
|
const _inFlightAgents = new Map<string, Promise<boolean>>();
|
||||||
|
let _initializeAppInFlight: Promise<void> | null = null;
|
||||||
|
|
||||||
export const useConfigStore = create<ConfigStore>()(
|
export const useConfigStore = create<ConfigStore>()(
|
||||||
devtools(
|
devtools(
|
||||||
@@ -1965,43 +1966,54 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
initializeApp: async () => {
|
initializeApp: async () => {
|
||||||
try {
|
if (_initializeAppInFlight) {
|
||||||
const debug = streamDebugEnabled();
|
return _initializeAppInFlight;
|
||||||
if (debug) console.log("Starting app initialization...");
|
}
|
||||||
|
|
||||||
const isConnected = await get().checkConnection();
|
const run = (async () => {
|
||||||
if (debug) console.log("Connection check result:", isConnected);
|
try {
|
||||||
|
const debug = streamDebugEnabled();
|
||||||
|
if (debug) console.log("Starting app initialization...");
|
||||||
|
|
||||||
if (!isConnected) {
|
const isConnected = await get().checkConnection();
|
||||||
if (debug) console.log("Server not connected");
|
if (debug) console.log("Connection check result:", isConnected);
|
||||||
// checkConnection already set lastDisconnectReason; do not overwrite.
|
|
||||||
|
if (!isConnected) {
|
||||||
|
if (debug) console.log("Server not connected");
|
||||||
|
// checkConnection already set lastDisconnectReason; do not overwrite.
|
||||||
|
set({
|
||||||
|
isConnected: false,
|
||||||
|
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug) console.log("Initializing app...");
|
||||||
|
await opencodeClient.initApp();
|
||||||
|
|
||||||
|
if (debug) console.log("Loading providers...");
|
||||||
|
await get().loadProviders();
|
||||||
|
|
||||||
|
if (debug) console.log("Loading agents...");
|
||||||
|
await get().loadAgents();
|
||||||
|
|
||||||
|
set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" });
|
||||||
|
if (debug) console.log("App initialized successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize app:", error);
|
||||||
set({
|
set({
|
||||||
|
isInitialized: false,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
||||||
|
lastDisconnectReason: 'init_error',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
})().finally(() => {
|
||||||
|
_initializeAppInFlight = null;
|
||||||
|
});
|
||||||
|
|
||||||
if (debug) console.log("Initializing app...");
|
_initializeAppInFlight = run;
|
||||||
await opencodeClient.initApp();
|
return run;
|
||||||
|
|
||||||
if (debug) console.log("Loading providers...");
|
|
||||||
await get().loadProviders();
|
|
||||||
|
|
||||||
if (debug) console.log("Loading agents...");
|
|
||||||
await get().loadAgents();
|
|
||||||
|
|
||||||
set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" });
|
|
||||||
if (debug) console.log("App initialized successfully");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to initialize app:", error);
|
|
||||||
set({
|
|
||||||
isInitialized: false,
|
|
||||||
isConnected: false,
|
|
||||||
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
|
||||||
lastDisconnectReason: 'init_error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentProvider: () => {
|
getCurrentProvider: () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user