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