perf: optimize app startup and background lifecycles

This commit is contained in:
inman
2026-08-19 00:56:15 +08:00
parent 31332f6d76
commit 927e13349b
121 changed files with 4359 additions and 1400 deletions

View File

@@ -3,29 +3,12 @@
* Handles routing and global providers
*/
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import { Component, useEffect, useState } from 'react';
import { Component, lazy, Suspense, useEffect, useState } from 'react';
import type { ErrorInfo, ReactNode } from 'react';
import { Toaster } from 'sonner';
import i18n from './i18n';
import { MainLayout } from './components/layout/MainLayout';
import { TooltipProvider } from '@/components/ui/tooltip';
import { Models } from './pages/Models';
import { Projects } from './pages/Projects';
import { Sessions } from './pages/Sessions';
import { Chat } from './pages/Chat';
import { Home } from './pages/Home';
import { PreviewScene } from './pages/Makelore';
import { ProjectConfiguration } from './pages/ProjectConfiguration';
import { Workbench } from './pages/Workbench';
import { ImageCanvas } from './pages/ImageCanvas';
import { ImagePromptMuseum } from './pages/ImagePromptMuseum';
import { AiHardware } from './pages/AiHardware';
import { Settings } from './pages/Settings';
import { Setup } from './pages/Setup';
import { Login } from './pages/Login';
import { ModuleSelection } from './pages/ModuleSelection';
import { Learning } from './pages/Learning';
import { LearningCoursePlayer } from './pages/Learning/CoursePlayer';
import { useSettingsStore } from './stores/settings';
import { useProviderStore } from './stores/providers';
import { useAuthStore } from './stores/auth';
@@ -40,8 +23,27 @@ import {
import { useUserSyncStore } from './stores/user-sync';
import { flushPendingAgentSessionSync } from '@/lib/agent-session-sync';
import { subscribeHostEvent } from '@/lib/host-events';
import { reportDesktopActivity, type DesktopActivityModule } from '@/lib/host-api';
import { installRendererPerformanceDiagnostics } from '@/lib/performance-diagnostics';
import { resolveSupportedLanguage } from '../shared/language';
const Models = lazy(() => import('./pages/Models').then(({ Models: component }) => ({ default: component })));
const Projects = lazy(() => import('./pages/Projects').then(({ Projects: component }) => ({ default: component })));
const Sessions = lazy(() => import('./pages/Sessions').then(({ Sessions: component }) => ({ default: component })));
const Chat = lazy(() => import('./pages/Chat').then(({ Chat: component }) => ({ default: component })));
const Home = lazy(() => import('./pages/Home').then(({ Home: component }) => ({ default: component })));
const PreviewScene = lazy(() => import('./pages/Makelore').then(({ PreviewScene: component }) => ({ default: component })));
const ProjectConfiguration = lazy(() => import('./pages/ProjectConfiguration').then(({ ProjectConfiguration: component }) => ({ default: component })));
const Workbench = lazy(() => import('./pages/Workbench').then(({ Workbench: component }) => ({ default: component })));
const ImageCanvas = lazy(() => import('./pages/ImageCanvas').then(({ ImageCanvas: component }) => ({ default: component })));
const ImagePromptMuseum = lazy(() => import('./pages/ImagePromptMuseum').then(({ ImagePromptMuseum: component }) => ({ default: component })));
const AiHardware = lazy(() => import('./pages/AiHardware').then(({ AiHardware: component }) => ({ default: component })));
const Settings = lazy(() => import('./pages/Settings').then(({ Settings: component }) => ({ default: component })));
const Setup = lazy(() => import('./pages/Setup').then(({ Setup: component }) => ({ default: component })));
const Login = lazy(() => import('./pages/Login').then(({ Login: component }) => ({ default: component })));
const ModuleSelection = lazy(() => import('./pages/ModuleSelection').then(({ ModuleSelection: component }) => ({ default: component })));
const Learning = lazy(() => import('./pages/Learning').then(({ Learning: component }) => ({ default: component })));
const LearningCoursePlayer = lazy(() => import('./pages/Learning/CoursePlayer').then(({ LearningCoursePlayer: component }) => ({ default: component })));
/**
* Error Boundary to catch and display React rendering errors
@@ -123,6 +125,26 @@ function getReturnPath(location: ReturnType<typeof useLocation>): string {
return `${location.pathname}${location.search}`;
}
function moduleForPath(pathname: string): DesktopActivityModule | null {
const module = getGuardedAiModuleForPath(pathname);
if (module === 'programming') return 'programming';
if (module === 'painting') return 'painting';
if (module === 'learning') return 'learning';
if (module === 'robot') return 'robot';
return null;
}
function publishDesktopActivity(input: {
visible: boolean;
module: DesktopActivityModule | null;
}): void {
try {
void Promise.resolve(reportDesktopActivity(input)).catch(() => undefined);
} catch {
// Renderer-only hosts may not expose lifecycle IPC; do not block routing.
}
}
function ProtectedLayout({
authReady,
authRequired,
@@ -262,6 +284,8 @@ function App() {
const authRequired = !skipSetupForE2E && !rendererOnlyPreview;
const imageWorkspaceLocalDevelopment = window.electron?.imageWorkspaceLocalDevelopment === true;
useEffect(() => installRendererPerformanceDiagnostics(), []);
useEffect(() => {
void initAuth();
}, [initAuth]);
@@ -273,6 +297,13 @@ function App() {
return unsubscribe;
}, []);
useEffect(() => subscribeHostEvent('lifecycle:sleep', () => {
// Main owns the runtime shutdown decision. Once it has confirmed that
// there are no leases, close renderer-owned SSE/listener state as well so
// a hidden shell cannot keep reconnecting against a stopped runtime.
useOpencodeStore.getState().pauseBackgroundStreams();
}), []);
useEffect(() => {
if (rendererOnlyPreview || !authInitialized || !authAccessToken) return;
@@ -347,6 +378,19 @@ function App() {
});
}, [authenticated, authInitialized, bootstrapUserSync, rendererOnlyPreview, setupReady]);
useEffect(() => {
if (rendererOnlyPreview) return undefined;
const publish = () => {
publishDesktopActivity({
visible: document.visibilityState === 'visible',
module: moduleForPath(location.pathname),
});
};
publish();
document.addEventListener('visibilitychange', publish, { passive: true });
return () => document.removeEventListener('visibilitychange', publish);
}, [location.pathname, rendererOnlyPreview]);
// Redirect to setup wizard if not complete
useEffect(() => {
if (rendererOnlyPreview && location.pathname.startsWith('/setup')) {
@@ -389,7 +433,8 @@ function App() {
return (
<ErrorBoundary>
<TooltipProvider delayDuration={300}>
<Routes>
<Suspense fallback={<StartupScreen />}>
<Routes>
{/* Setup wizard (shown on first launch) */}
<Route path="/setup/*" element={<Setup />} />
<Route path="/login" element={<Login />} />
@@ -435,7 +480,8 @@ function App() {
<Route path="/settings/*" element={<Settings />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</Routes>
</Suspense>
{/* Global toast notifications */}
<Toaster