perf: cut cold-start download 58% and startup heap 22% via measured chunk-graph fixes (#2742)
* fix(ui): update session-switch-resync test to current handleEvent/setSessionTodos signatures
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(build): split Shiki grammars/themes, CodeMirror legacy modes, and @pierre/diffs into on-demand chunks
Merging @shikijs/langs into one manual vendor chunk made the first language
request download every grammar (7.4 MB raw / 1 MB gzip). Letting Rollup split
these packages per dynamically imported module downloads only the languages,
themes, and modes actually used — matching how the worker build already
behaves. @pierre/diffs is split the same way so its pure patch parser (used by
the eager tool renderer) no longer drags the Shiki-importing render stack into
the startup graph.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(ui): restore lazy heavy views and stop eager settings-graph loading
- MainLayout: DiffView/FilesView/GitView/PlanView return to lazyWithChunkRecovery
(they were silently made static in 2031e3b4 while their Suspense wrappers
remained), keeping the CodeMirror and @pierre/diffs stacks out of startup.
- ContextPanel: same lazy treatment for its Diff/Files/Git/Plan/Walkthrough
tabs, with null Suspense fallbacks.
- CommandPalette imported getSettingsNavIcon from SettingsView, statically
pulling the entire settings surface (SkillsPage -> CodeMirrorEditor -> vim
mode, theme registry -> @pierre/diffs) into the eager graph; the helper now
lives in lib/settings/metadata.
- The windowed SettingsWindow mounts only after its first open: rendering the
lazy component closed made React fetch the SettingsView chunk graph at
startup.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(ui): keep @pierre/diffs + Shiki out of the eager chat graph and defer diff worker warmup
- DiffWorkerProvider no longer statically imports @pierre/diffs/worker or the
theme registry, and no longer spawns 3 workers plus a main-thread shared
highlighter during mount. Pools are created on demand through a dynamic
module load, warmed via requestIdleCallback after startup settles, and
useWorkerPool notifies consumers when a pool becomes available.
- ToolPart's rich diff preview moves to lazily loaded ToolPartDiffPreview;
the plain-text patch (PlainDiffFallback) renders while the chunk loads,
mirroring the existing error fallback. Theme registration happens during
render inside the lazy module so PatchDiff never renders unregistered ids.
- ChatInput mounts its lazy ToolOutputDialog only after the first attachment
preview opens instead of fetching the dialog chunk on the draft screen.
- getMarkdownSyntaxVars moves to a pierre-free markdownSyntaxVars module so
eager code-rendering consumers stop importing the registration module.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(web): load ghostty-web and Nerd Fonts on first terminal use
- ghostty-web (638 KB raw JS + WASM VT) is dynamically imported when a
terminal actually mounts; TerminalView stays eagerly importable for the
bottom dock.
- The ~2 MB of CDN Nerd Fonts are no longer preloaded and force-loaded on
every cold start. index.html exposes an idempotent
__openchamberEnsureNerdFonts hook; TerminalViewport requests it on mount
and waits up to 2s so a cached font is in place before the glyph atlas is
built, while a cold CDN fetch never blocks the terminal. Runtimes without
the hook (VS Code, mobile) resolve immediately, matching their existing
fallback-font behavior.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Serhii Dziupin
parent
da3d467f82
commit
bc380e6e1b
@@ -4,13 +4,18 @@ import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import { DiffViewIcon } from '@/components/icons/DiffIcon';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
|
||||
import { DiffView } from '@/components/views/DiffView';
|
||||
import { FilesView } from '@/components/views/FilesView';
|
||||
import { GitView } from '@/components/views/GitView';
|
||||
import { PullRequestView } from '@/components/views/PullRequestView';
|
||||
import { TerminalView } from '@/components/views/TerminalView';
|
||||
import { WalkthroughView } from '@/components/views/walkthrough/WalkthroughView';
|
||||
import { PlanView } from '@/components/views/PlanView';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
|
||||
// Heavy views stay on-demand (same as MainLayout): importing DiffView/FilesView
|
||||
// or the walkthrough statically pulls the CodeMirror and @pierre/diffs stacks
|
||||
// into the eager startup graph even when no such tab is open.
|
||||
const WalkthroughView = lazyWithChunkRecovery(() => import('@/components/views/walkthrough/WalkthroughView').then((m) => ({ default: m.WalkthroughView })));
|
||||
const DiffView = lazyWithChunkRecovery(() => import('@/components/views/DiffView').then((m) => ({ default: m.DiffView })));
|
||||
const FilesView = lazyWithChunkRecovery(() => import('@/components/views/FilesView').then((m) => ({ default: m.FilesView })));
|
||||
const GitView = lazyWithChunkRecovery(() => import('@/components/views/GitView').then((m) => ({ default: m.GitView })));
|
||||
const PlanView = lazyWithChunkRecovery(() => import('@/components/views/PlanView').then((m) => ({ default: m.PlanView })));
|
||||
import { ProjectContextPanel } from './RightSidebarTabs';
|
||||
import { SidebarFilesTree } from './SidebarFilesTree';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
@@ -2699,13 +2704,13 @@ export const ContextPanel: React.FC = () => {
|
||||
const activeNonChatContent = activeTab?.mode === 'context'
|
||||
? <ContextPanelContent />
|
||||
: activeTab?.mode === 'git'
|
||||
? <GitView isActive={isOpen} />
|
||||
? <React.Suspense fallback={null}><GitView isActive={isOpen} /></React.Suspense>
|
||||
: activeTab?.mode === 'pr'
|
||||
? <PullRequestView />
|
||||
: activeTab?.mode === 'notes'
|
||||
? <ProjectContextPanel />
|
||||
: activeTab?.mode === 'plan'
|
||||
? <PlanView targetPath={activeTab.targetPath} />
|
||||
? <React.Suspense fallback={null}><PlanView targetPath={activeTab.targetPath} /></React.Suspense>
|
||||
: activeTab?.mode === 'preview'
|
||||
? <PreviewPane rawUrl={activeTab.targetPath ?? ''} onNavigate={(url) => openContextPreview(effectiveDirectory, url)} />
|
||||
: (
|
||||
@@ -2917,7 +2922,7 @@ export const ContextPanel: React.FC = () => {
|
||||
<div className={cn('absolute inset-0 flex', isFileTabActive ? 'flex' : 'hidden')}>
|
||||
<div className="h-full min-w-0 flex-1">
|
||||
{hasOpenEditorFile ? (
|
||||
<FilesView mode="editor-only" />
|
||||
<React.Suspense fallback={null}><FilesView mode="editor-only" /></React.Suspense>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
||||
<Icon name="file-code" className="h-12 w-12 text-muted-foreground/50" />
|
||||
@@ -2983,16 +2988,18 @@ export const ContextPanel: React.FC = () => {
|
||||
activeTab?.id !== tab.id && 'hidden'
|
||||
)}
|
||||
>
|
||||
<DiffView
|
||||
hideStackedFileSidebar
|
||||
stackedDefaultCollapsedAll
|
||||
pinSelectedFileHeaderToTopOnNavigate
|
||||
showOpenInEditorAction
|
||||
diffScope={tab.diffScope ?? (tab.stagedDiff ? 'staged' : 'working')}
|
||||
onDiffScopeChange={handleDiffScopeChange}
|
||||
targetFilePath={tab.targetPath}
|
||||
flushContent
|
||||
/>
|
||||
<React.Suspense fallback={null}>
|
||||
<DiffView
|
||||
hideStackedFileSidebar
|
||||
stackedDefaultCollapsedAll
|
||||
pinSelectedFileHeaderToTopOnNavigate
|
||||
showOpenInEditorAction
|
||||
diffScope={tab.diffScope ?? (tab.stagedDiff ? 'staged' : 'working')}
|
||||
onDiffScopeChange={handleDiffScopeChange}
|
||||
targetFilePath={tab.targetPath}
|
||||
flushContent
|
||||
/>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
))}
|
||||
{hasTerminalTab ? (
|
||||
@@ -3002,7 +3009,9 @@ export const ContextPanel: React.FC = () => {
|
||||
) : null}
|
||||
{hasWalkthroughTab ? (
|
||||
<div className={cn('absolute inset-0', activeTab?.mode === 'walkthrough' ? 'block' : 'hidden')}>
|
||||
<WalkthroughView directory={effectiveDirectory} />
|
||||
<React.Suspense fallback={null}>
|
||||
<WalkthroughView directory={effectiveDirectory} />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
) : null}
|
||||
{activeTab?.mode !== 'chat' && !isFileTabActive && activeTab?.mode !== 'browser' && activeTab?.mode !== 'diff' && activeTab?.mode !== 'terminal' && activeTab?.mode !== 'walkthrough' ? activeNonChatContent : null}
|
||||
|
||||
@@ -29,14 +29,16 @@ import { cn } from '@/lib/utils';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
|
||||
import { ChatView } from '@/components/views/ChatView';
|
||||
import { DiffView } from '@/components/views/DiffView';
|
||||
import { FilesView } from '@/components/views/FilesView';
|
||||
import { GitView } from '@/components/views/GitView';
|
||||
import { PlanView } from '@/components/views/PlanView';
|
||||
|
||||
// Keep TerminalView eager: the bottom dock reserves its height immediately, so
|
||||
// suspending here leaves a large blank panel on slower machines.
|
||||
// Other heavy views stay on-demand to reduce initial bundle parse time.
|
||||
// Other heavy views stay on-demand to reduce initial bundle parse time:
|
||||
// DiffView/FilesView pull the CodeMirror and @pierre/diffs stacks into the
|
||||
// startup graph when imported statically.
|
||||
const PlanView = lazyWithChunkRecovery(() => import('@/components/views/PlanView').then(m => ({ default: m.PlanView })));
|
||||
const GitView = lazyWithChunkRecovery(() => import('@/components/views/GitView').then(m => ({ default: m.GitView })));
|
||||
const DiffView = lazyWithChunkRecovery(() => import('@/components/views/DiffView').then(m => ({ default: m.DiffView })));
|
||||
const FilesView = lazyWithChunkRecovery(() => import('@/components/views/FilesView').then(m => ({ default: m.FilesView })));
|
||||
const DiagramView = lazyWithChunkRecovery(() => import('@/components/views/DiagramView').then(m => ({ default: m.DiagramView })));
|
||||
const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView })));
|
||||
const SettingsWindow = lazyWithChunkRecovery(() => import('@/components/views/SettingsWindow').then(m => ({ default: m.SettingsWindow })));
|
||||
@@ -48,6 +50,17 @@ export const MainLayout: React.FC = () => {
|
||||
const isSessionSwitcherOpen = useUIStore((state) => state.isSessionSwitcherOpen);
|
||||
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
|
||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||
// Mount the windowed settings dialog only after its first open: rendering
|
||||
// the lazy component (even closed) makes React fetch the SettingsView
|
||||
// chunk graph (CodeMirror editor, vim mode, theme tooling) on startup.
|
||||
// Once opened it stays mounted so the close animation and state behave as
|
||||
// before.
|
||||
const [settingsWindowMounted, setSettingsWindowMounted] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
if (isSettingsDialogOpen) {
|
||||
setSettingsWindowMounted(true);
|
||||
}
|
||||
}, [isSettingsDialogOpen]);
|
||||
const isMultiRunLauncherOpen = useUIStore((state) => state.isMultiRunLauncherOpen);
|
||||
const setMultiRunLauncherOpen = useUIStore((state) => state.setMultiRunLauncherOpen);
|
||||
const multiRunLauncherPrefillPrompt = useUIStore((state) => state.multiRunLauncherPrefillPrompt);
|
||||
@@ -464,12 +477,14 @@ export const MainLayout: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Desktop settings: windowed dialog with blur */}
|
||||
<React.Suspense fallback={null}>
|
||||
<SettingsWindow
|
||||
open={isSettingsDialogOpen}
|
||||
onOpenChange={setSettingsDialogOpen}
|
||||
/>
|
||||
</React.Suspense>
|
||||
{settingsWindowMounted ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<SettingsWindow
|
||||
open={isSettingsDialogOpen}
|
||||
onOpenChange={setSettingsDialogOpen}
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user