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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user