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
@@ -1,5 +1,8 @@
import type { Extension } from '@codemirror/state';
// Static imports for the most common languages only.
// Less common languages are loaded dynamically via loadLanguageByExtension
// to keep the initial bundle lean.
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { css } from '@codemirror/lang-css';
@@ -7,33 +10,12 @@ import { html } from '@codemirror/lang-html';
import { markdown } from '@codemirror/lang-markdown';
import { languages } from '@codemirror/language-data';
import { python } from '@codemirror/lang-python';
import { sql } from '@codemirror/lang-sql';
import { xml } from '@codemirror/lang-xml';
import { yaml as yamlLanguage } from '@codemirror/lang-yaml';
import { rust } from '@codemirror/lang-rust';
import { elixir } from 'codemirror-lang-elixir';
import { cpp } from '@codemirror/lang-cpp';
import { go } from '@codemirror/lang-go';
import { Language, LanguageDescription, StreamLanguage, HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { toml } from '@codemirror/legacy-modes/mode/toml';
import { diff } from '@codemirror/legacy-modes/mode/diff';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
import { properties } from '@codemirror/legacy-modes/mode/properties';
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
const shellLanguage = StreamLanguage.define(shell);
const tomlLanguage = StreamLanguage.define(toml);
const diffLanguage = StreamLanguage.define(diff);
const dockerfileLanguage = StreamLanguage.define(dockerFile);
const rubyLanguage = StreamLanguage.define(ruby);
const propertiesLanguage = StreamLanguage.define(properties);
const elixirSupport = elixir();
const elixirLanguage = elixirSupport.language;
const erlangLanguage = StreamLanguage.define(erlang);
function codeBlockLanguageResolver(info: string): Language | LanguageDescription | null {
const normalized = info.trim().toLowerCase();
@@ -46,11 +28,6 @@ function codeBlockLanguageResolver(info: string): Language | LanguageDescription
case 'shellsession':
case 'console':
return shellLanguage;
case 'toml':
return tomlLanguage;
case 'diff':
case 'patch':
return diffLanguage;
case 'json':
case 'jsonc':
case 'json5':
@@ -65,39 +42,13 @@ function codeBlockLanguageResolver(info: string): Language | LanguageDescription
return javascript({ typescript: true }).language;
case 'tsx':
return javascript({ typescript: true, jsx: true }).language;
case 'yaml':
case 'yml':
return yamlLanguage().language;
case 'html':
return html().language;
case 'css':
return css().language;
case 'xml':
case 'svg':
return xml().language;
case 'py':
case 'python':
return python().language;
case 'sql':
return sql().language;
case 'rs':
case 'rust':
return rust().language;
case 'c':
case 'cpp':
case 'h':
case 'hpp':
return cpp().language;
case 'go':
return go().language;
case 'ex':
case 'exs':
case 'elixir':
return elixirLanguage;
case 'erl':
case 'hrl':
case 'erlang':
return erlangLanguage;
case 'heex':
case 'eex':
case 'leex':
@@ -135,8 +86,6 @@ export function languageByExtension(filePath: string): Extension | null {
// Special filenames
switch (filename) {
case 'dockerfile':
return dockerfileLanguage;
case 'makefile':
case 'gnumakefile':
// No dedicated mode; shell is a decent fallback for Make-ish files.
@@ -147,7 +96,7 @@ export function languageByExtension(filePath: string): Extension | null {
const ext = idx >= 0 ? normalized.slice(idx + 1) : '';
switch (ext) {
// JavaScript/TypeScript
// JavaScript/TypeScript (most common — keep static)
case 'ts':
case 'tsx':
case 'mts':
@@ -159,7 +108,7 @@ export function languageByExtension(filePath: string): Extension | null {
case 'cjs':
return javascript({ typescript: false, jsx: ext === 'jsx' });
// Web
// Web (keep static)
case 'json':
case 'jsonc':
case 'json5':
@@ -187,20 +136,7 @@ export function languageByExtension(filePath: string): Extension | null {
markdownHighlight(),
];
// Data/config
case 'yml':
case 'yaml':
return yamlLanguage();
case 'toml':
return tomlLanguage;
case 'ini':
case 'cfg':
case 'conf':
case 'config':
case 'properties':
return propertiesLanguage;
// Shell
// Shell (keep static)
case 'sh':
case 'bash':
case 'zsh':
@@ -208,52 +144,14 @@ export function languageByExtension(filePath: string): Extension | null {
case 'env':
return shellLanguage;
// Languages we already ship
// Python (very common — keep static)
case 'py':
case 'pyw':
case 'pyi':
return python();
case 'sql':
case 'psql':
case 'plsql':
return sql();
case 'xml':
case 'xsl':
case 'xslt':
case 'xsd':
case 'dtd':
case 'plist':
case 'svg':
return xml();
case 'rs':
return rust();
case 'c':
case 'cpp':
case 'h':
case 'hpp':
return cpp();
case 'go':
return go();
// Legacy modes
case 'rb':
case 'erb':
case 'rake':
case 'gemspec':
return rubyLanguage;
case 'ex':
case 'exs':
return elixirSupport;
case 'erl':
case 'hrl':
return erlangLanguage;
case 'eex':
case 'leex':
case 'heex':
return html();
// Less common languages: return null so callers fall back to
// loadLanguageByExtension which dynamically imports from @codemirror/language-data.
default:
return null;
}