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:
committed by
GitHub
co-authored by
Shyamalan Kannan
parent
522cebf127
commit
ecb22e19c3
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user