perf: optimize app startup and background lifecycles
This commit is contained in:
86
src/App.tsx
86
src/App.tsx
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user