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
+92 -63
View File
@@ -143,12 +143,15 @@ export async function bootstrapDirectory(input: {
}
if (loading) set({ status: "partial" })
const results = await Promise.allSettled([
// ---------------------------------------------------------------------------
// Phase 1: Critical path — block until these resolve so the UI can render.
// These are the minimum data needed to show a functional chat interface.
// ---------------------------------------------------------------------------
const phase1Results = await Promise.allSettled([
seededProject
? Promise.resolve()
: retry(() => sdk.project.current().then((x) => set({ project: unwrap(x, "project.current").id }))),
retry(() => sdk.provider.list().then((x) => set({ provider: unwrap(x, "provider.list") }))),
retry(() => sdk.app.agents().then((x) => set({ agent: unwrap(x, "app.agents") }))),
retry(() => sdk.config.get().then((x) => set({ config: unwrap(x, "config.get") }))),
retry(() =>
sdk.path.get().then((x) => {
@@ -158,15 +161,37 @@ export async function bootstrapDirectory(input: {
if (next) set({ project: next })
}),
),
retry(() => sdk.command.list().then((x) => set({ command: unwrap(x, "command.list") }))),
retry(() => sdk.session.status().then((x) => set({ session_status: unwrap(x, "session.status") }))),
input.loadSessions(directory),
])
const phase1Errors = phase1Results
.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"
if (phase1Errors.length === phase1Results.length || criticalPhase1Failed) {
console.error(`[bootstrap] directory bootstrap failed for ${directory}`, phase1Errors[0])
return
}
// Mark ready after critical data arrives so the UI can paint.
if (loading) set({ status: "complete" })
// ---------------------------------------------------------------------------
// Phase 2: Deferrable — fetch after first paint without blocking.
// These enrich the UI but aren't required for basic functionality.
// ---------------------------------------------------------------------------
void Promise.allSettled([
retry(() => sdk.app.agents().then((x) => set({ agent: unwrap(x, "app.agents") }))),
retry(() => sdk.command.list().then((x) => set({ command: unwrap(x, "command.list") }))),
retry(() => sdk.mcp.status().then((x) => set({ mcp: unwrap(x, "mcp.status") }))),
retry(() => sdk.lsp.status().then((x) => set({ lsp: unwrap(x, "lsp.status") }))),
retry(() =>
sdk.vcs.get().then((x) => {
const current = getState()
// vcs is optional — fall back to current if server omits it.
if (x.error) {
throw new Error(`vcs.get failed: ${String(x.error)}`)
}
@@ -179,30 +204,30 @@ export async function bootstrapDirectory(input: {
Object.entries(before.question ?? {}).map(([sessionID, questions]) => [sessionID, requestSignature(questions)]),
)
const x = await sdk.question.list(directory ? { directory } : undefined)
if (x.error) {
const status = (x as { response?: { status?: number } }).response?.status
const err = new Error(`question.list failed${status ? ` (${status})` : ""}: ${String(x.error)}`)
if (status !== undefined) (err as Error & { status?: number }).status = status
throw err
}
const grouped = groupBySession(
(x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID),
)
const current = getState()
const merged = { ...current.question }
for (const [sessionID, questions] of Object.entries(grouped)) {
merged[sessionID] = questions
.filter((q) => !!q?.id)
.sort((a, b) => cmp(a.id, b.id))
}
for (const sessionID of beforeSignatures.keys()) {
if (grouped[sessionID]) continue
const beforeSignature = beforeSignatures.get(sessionID) ?? ""
const currentSignature = requestSignature(current.question[sessionID])
if (currentSignature !== beforeSignature) continue
delete merged[sessionID]
}
set({ question: merged })
if (x.error) {
const status = (x as { response?: { status?: number } }).response?.status
const err = new Error(`question.list failed${status ? ` (${status})` : ""}: ${String(x.error)}`)
if (status !== undefined) (err as Error & { status?: number }).status = status
throw err
}
const grouped = groupBySession(
(x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID),
)
const current = getState()
const merged = { ...current.question }
for (const [sessionID, questions] of Object.entries(grouped)) {
merged[sessionID] = questions
.filter((q) => !!q?.id)
.sort((a, b) => cmp(a.id, b.id))
}
for (const sessionID of beforeSignatures.keys()) {
if (grouped[sessionID]) continue
const beforeSignature = beforeSignatures.get(sessionID) ?? ""
const currentSignature = requestSignature(current.question[sessionID])
if (currentSignature !== beforeSignature) continue
delete merged[sessionID]
}
set({ question: merged })
}),
retry(async () => {
const before = getState()
@@ -210,40 +235,44 @@ export async function bootstrapDirectory(input: {
Object.entries(before.permission ?? {}).map(([sessionID, permissions]) => [sessionID, requestSignature(permissions)]),
)
const x = await sdk.permission.list(directory ? { directory } : undefined)
if (x.error) {
const status = (x as { response?: { status?: number } }).response?.status
const err = new Error(`permission.list failed${status ? ` (${status})` : ""}: ${String(x.error)}`)
if (status !== undefined) (err as Error & { status?: number }).status = status
throw err
}
const grouped = groupBySession(
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm?.sessionID),
)
const current = getState()
const merged = { ...current.permission }
for (const [sessionID, perms] of Object.entries(grouped)) {
merged[sessionID] = perms
.filter((p) => !!p?.id)
.sort((a, b) => cmp(a.id, b.id))
}
for (const sessionID of beforeSignatures.keys()) {
if (grouped[sessionID]) continue
const beforeSignature = beforeSignatures.get(sessionID) ?? ""
const currentSignature = requestSignature(current.permission[sessionID])
if (currentSignature !== beforeSignature) continue
delete merged[sessionID]
}
set({ permission: merged })
if (x.error) {
const status = (x as { response?: { status?: number } }).response?.status
const err = new Error(`permission.list failed${status ? ` (${status})` : ""}: ${String(x.error)}`)
if (status !== undefined) (err as Error & { status?: number }).status = status
throw err
}
const grouped = groupBySession(
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm?.sessionID),
)
const current = getState()
const merged = { ...current.permission }
for (const [sessionID, perms] of Object.entries(grouped)) {
merged[sessionID] = perms
.filter((p) => !!p?.id)
.sort((a, b) => cmp(a.id, b.id))
}
for (const sessionID of beforeSignatures.keys()) {
if (grouped[sessionID]) continue
const beforeSignature = beforeSignatures.get(sessionID) ?? ""
const currentSignature = requestSignature(current.permission[sessionID])
if (currentSignature !== beforeSignature) continue
delete merged[sessionID]
}
set({ permission: merged })
}),
])
]).then((results) => {
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === "rejected")
.map((r) => r.reason)
if (errors.length) {
console.error(`[bootstrap] deferred phase failed for ${directory}`, errors[0])
}
})
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === "rejected")
.map((r) => r.reason)
if (errors.length) {
console.error(`[bootstrap] directory bootstrap failed for ${directory}`, errors[0])
return
}
if (loading) set({ status: "complete" })
// ---------------------------------------------------------------------------
// Phase 3: Lazy — session list can be large; don't block on it.
// ---------------------------------------------------------------------------
void Promise.resolve(input.loadSessions(directory)).catch((err) => {
console.error(`[bootstrap] session load failed for ${directory}`, err)
})
}