perf: lazy-load heavy dependencies (MarkdownRenderer + CodeMirror languages) (#997)

* 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.

* perf: lazy-load heavy dependencies (MarkdownRenderer + CodeMirror languages)

MarkdownRenderer dynamic import:
- Move heavy implementation (marked, react-markdown, beautiful-mermaid,
  react-syntax-highlighter, ~1500 lines) to MarkdownRendererImpl.tsx
- Replace MarkdownRenderer.tsx with thin lazy wrapper using React.lazy
- All 11 existing imports work unchanged — no consumer code modified
- Full markdown stack loads on first render of markdown content

CodeMirror language lazy loading:
- languageByExtension.ts: remove static imports for 10+ less-common
  language packages (@codemirror/lang-go, lang-rust, lang-sql, etc.)
- Keep only 6 most common languages static: javascript, json, css, html,
  markdown, python, shell
- Less common languages return null from languageByExtension, causing
  callers to fall back to loadLanguageByExtension which dynamically
  loads from @codemirror/language-data
- Reduces initial bundle by ~200KB+ of language parsers

---------

Co-authored-by: Shyamalan Kannan <yabuku@Shyamalans-MacBook-Pro.local>
This commit is contained in:
Shyamalan Kannan
2026-04-23 12:31:42 +03:00
committed by GitHub
co-authored by Shyamalan Kannan
parent 522cebf127
commit ecb22e19c3
12 changed files with 1869 additions and 1809 deletions
+37 -20
View File
@@ -31,7 +31,6 @@ import {
type BootInjectionStatus,
type DesktopBootView,
} from '@/lib/desktopBoot';
import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen';
import type { RecoveryVariant } from '@/components/onboarding/DesktopConnectionRecovery';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
@@ -56,6 +55,11 @@ import { QuickOpenDialog } from '@/components/ui/QuickOpenDialog';
import { McpOAuthCallbackPage } from '@/components/sections/mcp/McpOAuthCallbackPage';
import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth';
// Lazy-loaded heavy views — loaded on demand to reduce initial bundle size.
const OnboardingScreen = React.lazy(() =>
import('@/components/onboarding/OnboardingScreen').then((m) => ({ default: m.OnboardingScreen })),
);
const AboutDialogWrapper: React.FC = () => {
const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen);
const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen);
@@ -645,13 +649,15 @@ function App({ apis }: AppProps) {
return (
<ErrorBoundary>
<div className="h-full text-foreground bg-transparent">
<OnboardingScreen
mode="first-launch"
onCliAvailable={handleDesktopBootDismiss}
onChooseRemote={() => {
// Switch to remote tab - handled internally by OnboardingScreen
}}
/>
<React.Suspense fallback={<div className="h-full" />}>
<OnboardingScreen
mode="first-launch"
onCliAvailable={handleDesktopBootDismiss}
onChooseRemote={() => {
// Switch to remote tab - handled internally by OnboardingScreen
}}
/>
</React.Suspense>
</div>
</ErrorBoundary>
);
@@ -664,13 +670,15 @@ function App({ apis }: AppProps) {
return (
<ErrorBoundary>
<div className="h-full text-foreground bg-transparent">
<OnboardingScreen
mode="recovery"
recoveryVariant={recoveryVariant}
recoveryHostUrl={hostUrl}
recoveryHostLabel={undefined}
onCliAvailable={handleDesktopBootDismiss}
/>
<React.Suspense fallback={<div className="h-full" />}>
<OnboardingScreen
mode="recovery"
recoveryVariant={recoveryVariant}
recoveryHostUrl={hostUrl}
recoveryHostLabel={undefined}
onCliAvailable={handleDesktopBootDismiss}
/>
</React.Suspense>
</div>
</ErrorBoundary>
);
@@ -747,6 +755,11 @@ function App({ apis }: AppProps) {
);
}
// Always mount the full provider tree to avoid remounts when isInitialized
// flips from false → true. FireworksProvider and VoiceProvider are lightweight
// shells; their heavy children are only activated when actually needed.
const isBootShell = !isInitialized && !isDesktopRuntime;
return (
<ErrorBoundary>
<SyncProvider sdk={opencodeClient.getSdkClient()} directory={currentDirectory || ''}>
@@ -758,11 +771,15 @@ function App({ apis }: AppProps) {
<SyncAppEffects embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled} />
<MainLayout />
<Toaster />
<ConfigUpdateOverlay />
<QuickOpenDialog />
<AboutDialogWrapper />
{showMemoryDebug && (
<MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} />
{!isBootShell && (
<>
<ConfigUpdateOverlay />
<QuickOpenDialog />
<AboutDialogWrapper />
{showMemoryDebug && (
<MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} />
)}
</>
)}
</div>
</TooltipProvider>