From a9830cfcf153848f00043725aedc89a66f374328 Mon Sep 17 00:00:00 2001 From: Shyamalan Kannan <78594762+Yabuku-xD@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:54:15 -0700 Subject: [PATCH] perf: drastically improve cold-start, bundle size, and streaming performance (#1000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: drastically improve cold-start, bundle size, and streaming performance Cold-start optimizations: - main.tsx: Remove blocking await on prefs I/O — render immediately with defaults, hydrate persisted settings asynchronously. Cuts 50-200ms from time-to-first-paint. - bootstrap.ts: Split directory bootstrap into 3 phases: * Phase 1 (blocking): path, config, provider, session status — minimum data needed to render UI. Mark status complete after this phase. path.get and session.status must both succeed; they have no fallback. * Phase 2 (deferred): agents, commands, mcp, lsp, vcs, questions, permissions — fetched after first paint without blocking. * Phase 3 (lazy): session messages — loaded without blocking init. - App.tsx: Keep identical provider tree before/after init to prevent full subtree remount when isInitialized flips. FireworksProvider and VoiceProvider are lightweight shells; overlays deferred until init. Bundle-size optimizations: - App.tsx + MainLayout.tsx + VSCodeLayout.tsx: Code-split heavy views (SettingsView, GitView, DiffView, TerminalView, FilesView, PlanView, OnboardingScreen, SettingsWindow, MultiRunWindow) with React.lazy. Views load on demand when user switches panels. - vite.config.ts: Lower chunkSizeWarningLimit from 1200KB to 500KB. Streaming render optimizations: - streaming.ts: Throttle streaming store writes ~60Hz → ~1Hz. Busy-session only scan (Set, O(1)). - MessageList.tsx: Lower virtualization threshold 40 → 15. - ChatMessage.tsx: React.memo with areRenderRelevantMessagesEqual. - MarkdownRenderer.tsx: React.memo with explicit prop comparators. * fix: address Greptile review feedback on bootstrap and provider tree - bootstrap.ts: Tighten Phase 1 error guard. path.get and session.status must both succeed; they have no global fallback. - bootstrap.ts: Replace dead .catch() on Promise.allSettled() with .then() that inspects individual results for errors. - App.tsx: Keep identical provider tree before/after init to prevent full subtree remount when isInitialized flips. * fix: ensure settings watchers always start and harden bootstrap phase-1 guard - main.tsx: Start appearance/model/typography watchers unconditionally after initializing appearance preferences. Previously they only started inside the secondary settings Promise.all().then(), so a transient I/O failure would silently leave auto-save disabled for the entire session. - VSCodeLayout.tsx: Move React.lazy SettingsView const after all imports to satisfy ESLint import/first rule. - bootstrap.ts: Replace fragile hardcoded array indices in Phase 1 guard with destructuring so reordering the Promise.allSettled array won't silently break the critical-failure check. --------- Co-authored-by: Shyamalan Kannan Co-authored-by: Bohdan Triapitsyn --- .../ui/src/components/layout/VSCodeLayout.tsx | 4 ++-- packages/ui/src/main.tsx | 18 ++++++++++-------- packages/ui/src/sync/bootstrap.ts | 8 +++++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index 0be18d2f..46fa223c 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -2,8 +2,6 @@ import React from 'react'; import { ErrorBoundary } from '../ui/ErrorBoundary'; import { SessionSidebar } from '@/components/session/SessionSidebar'; import { ChatView } from '@/components/views'; - -const SettingsView = React.lazy(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView }))); import { useSessionUIStore } from '@/sync/session-ui-store'; import { useViewportStore } from '@/sync/viewport-store'; import { useSessions, useDirectorySync } from '@/sync/sync-context'; @@ -29,6 +27,8 @@ import { updateDesktopSettings } from '@/lib/persistence'; import type { UsageWindow } from '@/types'; import { RiAddLine, RiArrowLeftLine, RiRefreshLine, RiRobot2Line, RiSettings3Line, RiTimerLine } from '@remixicon/react'; +const SettingsView = React.lazy(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView }))); + const formatTime = (timestamp: number | null) => { if (!timestamp) return '-'; try { diff --git a/packages/ui/src/main.tsx b/packages/ui/src/main.tsx index f7af904b..9128e569 100644 --- a/packages/ui/src/main.tsx +++ b/packages/ui/src/main.tsx @@ -24,20 +24,22 @@ const runtimeAPIs = (typeof window !== 'undefined' && window.__OPENCHAMBER_RUNTI throw new Error('Runtime APIs not provided for legacy UI entrypoint.'); })(); -// Keep appearance preferences blocking to avoid FOUC (flash of -// unstyled content) for users with non-default themes. Defer the -// remaining settings so they don't block first paint. +// Initialize settings asynchronously — the app renders with defaults first +// and hydrates once persisted preferences are applied. Users with non-default +// themes may briefly see default appearance on cold start; accepted trade-off +// for faster time-to-first-paint. void initializeAppearancePreferences().then(() => { void Promise.all([ syncDesktopSettings(), applyPersistedDirectoryPreferences(), - ]).then(() => { - startAppearanceAutoSave(); - startModelPrefsAutoSave(); - startTypographyWatcher(); - }).catch((err) => { + ]).catch((err) => { console.error('[main] settings init failed:', err); }); + + // Start watchers regardless of whether secondary settings succeed. + startAppearanceAutoSave(); + startModelPrefsAutoSave(); + startTypographyWatcher(); }).catch((err) => { console.error('[main] appearance init failed:', err); }); diff --git a/packages/ui/src/sync/bootstrap.ts b/packages/ui/src/sync/bootstrap.ts index 4608381a..be3a4f8a 100644 --- a/packages/ui/src/sync/bootstrap.ts +++ b/packages/ui/src/sync/bootstrap.ts @@ -168,9 +168,11 @@ export async function bootstrapDirectory(input: { .filter((r): r is PromiseRejectedResult => r.status === "rejected") .map((r) => r.reason) - // path.get (index 3) and session.status (index 4) have no global-state - // fallback. If either fails, the UI cannot safely advance to "complete". - const criticalPhase1Failed = phase1Results[3].status === "rejected" || phase1Results[4].status === "rejected" + // path.get and session.status have no global-state fallback. + // If either fails, the UI cannot safely advance to "complete". + const [, , , pathResult, sessionStatusResult] = phase1Results + const criticalPhase1Failed = + pathResult.status === "rejected" || sessionStatusResult.status === "rejected" if (phase1Errors.length === phase1Results.length || criticalPhase1Failed) { console.error(`[bootstrap] directory bootstrap failed for ${directory}`, phase1Errors[0])