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)
})
}
+64 -64
View File
@@ -35,80 +35,80 @@ export const useStreamingStore = create<StreamingStore>()(() => ({
* Called from the SyncBridge/flush handler when child store state changes.
* Derives streaming state from session_status + messages.
*/
/** Only update lastUpdateAt every this many ms to avoid 60Hz store churn */
const STREAMING_HEARTBEAT_MS = 1000
export function updateStreamingState(state: State) {
const now = Date.now()
const currentStore = useStreamingStore.getState()
const currentStreamingIds = currentStore.streamingMessageIds
const currentStreamStates = currentStore.messageStreamStates
const nextStreamingIds = new Map<string, string | null>()
const nextStreamStates = new Map(useStreamingStore.getState().messageStreamStates)
const nextStreamStates = new Map(currentStreamStates)
let changed = false
// Fast path: only scan sessions that are actually busy.
// Idle sessions are handled by checking against currentStreamingIds below.
const busySessionIds = new Set<string>()
for (const [sessionID, status] of Object.entries(state.session_status ?? {})) {
const isBusy = (status as SessionStatus).type === "busy"
const messages = state.message[sessionID]
if (isBusy && messages && messages.length > 0) {
// Find the last assistant message — that's the one streaming
let streamingMsg: Message | null = null
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "assistant") {
streamingMsg = messages[i]
break
}
}
if (streamingMsg) {
const prevId = nextStreamingIds.get(sessionID)
if (prevId !== streamingMsg.id) changed = true
nextStreamingIds.set(sessionID, streamingMsg.id)
const existing = nextStreamStates.get(streamingMsg.id)
if (!existing || existing.phase !== "streaming") {
nextStreamStates.set(streamingMsg.id, {
phase: "streaming",
startedAt: existing?.startedAt ?? now,
lastUpdateAt: now,
})
changed = true
} else if (existing.lastUpdateAt !== now) {
nextStreamStates.set(streamingMsg.id, {
...existing,
lastUpdateAt: now,
})
changed = true
}
}
} else {
// Session is idle — check if we had a streaming message
const prev = useStreamingStore.getState().streamingMessageIds.get(sessionID)
if (prev) {
nextStreamingIds.set(sessionID, null)
const existing = nextStreamStates.get(prev)
if (existing && existing.phase === "streaming") {
// Transition to cooldown then completed
nextStreamStates.set(prev, {
...existing,
phase: "completed",
completedAt: now,
})
changed = true
}
}
if ((status as SessionStatus).type === "busy") {
busySessionIds.add(sessionID)
}
}
// Also mark completed any streaming messages for sessions no longer in status
const currentIds = useStreamingStore.getState().streamingMessageIds
for (const [sessionID, msgId] of currentIds) {
if (msgId && !state.session_status?.[sessionID]) {
const existing = nextStreamStates.get(msgId)
if (existing && existing.phase === "streaming") {
nextStreamStates.set(msgId, {
...existing,
phase: "completed",
completedAt: now,
})
changed = true
for (const sessionID of busySessionIds) {
const messages = state.message[sessionID]
if (!messages || messages.length === 0) continue
// Find the last assistant message — that's the one streaming
let streamingMsg: Message | null = null
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "assistant") {
streamingMsg = messages[i]
break
}
nextStreamingIds.set(sessionID, null)
}
if (!streamingMsg) continue
const prevId = currentStreamingIds.get(sessionID)
if (prevId !== streamingMsg.id) changed = true
nextStreamingIds.set(sessionID, streamingMsg.id)
const existing = nextStreamStates.get(streamingMsg.id)
if (!existing || existing.phase !== "streaming") {
nextStreamStates.set(streamingMsg.id, {
phase: "streaming",
startedAt: existing?.startedAt ?? now,
lastUpdateAt: now,
})
changed = true
} else if (now - existing.lastUpdateAt >= STREAMING_HEARTBEAT_MS) {
// Throttle lastUpdateAt writes to ~1Hz instead of 60Hz
nextStreamStates.set(streamingMsg.id, {
...existing,
lastUpdateAt: now,
})
changed = true
}
}
// Mark completed any previously streaming sessions that are now idle or gone
for (const [sessionID, msgId] of currentStreamingIds) {
if (!msgId) continue
const isStillBusy = busySessionIds.has(sessionID)
if (isStillBusy) continue
nextStreamingIds.set(sessionID, null)
const existing = nextStreamStates.get(msgId)
if (existing && existing.phase === "streaming") {
nextStreamStates.set(msgId, {
...existing,
phase: "completed",
completedAt: now,
})
changed = true
}
}