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
@@ -2,7 +2,6 @@
|
||||
import React from 'react';
|
||||
import { useMobileAppActions } from '@/apps/mobileAppContext';
|
||||
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
||||
import { PatchDiff } from '@pierre/diffs/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
|
||||
import { MessageFilesDisplay } from '../../FileAttachment';
|
||||
@@ -10,7 +9,6 @@ import { getToolMetadata } from '@/lib/toolHelpers';
|
||||
import type { ToolPart as ToolPartType, ToolState as ToolStateUnion, FilePart } from '@opencode-ai/sdk/v2';
|
||||
import { toolDisplayStyles } from '@/lib/typography';
|
||||
import { WorkerHighlightedCode } from '@/components/code/WorkerHighlightedCode';
|
||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useSessionMessageRecords, useEnsureSessionMessages } from '@/sync/sync-context';
|
||||
@@ -24,8 +22,8 @@ import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatAutoFollow';
|
||||
import type { ToolPopupContent } from '../types';
|
||||
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||
import { PlainDiffFallback } from './PlainDiffFallback';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
|
||||
import {
|
||||
formatEditOutput,
|
||||
@@ -385,40 +383,6 @@ const getToolDiagnosticSection = (
|
||||
};
|
||||
};
|
||||
|
||||
const usePierreThemeConfig = () => {
|
||||
const themeSystem = useOptionalThemeSystem();
|
||||
const fallbackLightTheme = React.useMemo(() => getDefaultTheme(false), []);
|
||||
const fallbackDarkTheme = React.useMemo(() => getDefaultTheme(true), []);
|
||||
|
||||
const availableThemes = React.useMemo(
|
||||
() => themeSystem?.availableThemes ?? [fallbackLightTheme, fallbackDarkTheme],
|
||||
[fallbackDarkTheme, fallbackLightTheme, themeSystem?.availableThemes],
|
||||
);
|
||||
const lightThemeId = themeSystem?.lightThemeId ?? fallbackLightTheme.metadata.id;
|
||||
const darkThemeId = themeSystem?.darkThemeId ?? fallbackDarkTheme.metadata.id;
|
||||
|
||||
const lightTheme = React.useMemo(
|
||||
() => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? fallbackLightTheme,
|
||||
[availableThemes, fallbackLightTheme, lightThemeId],
|
||||
);
|
||||
const darkTheme = React.useMemo(
|
||||
() => availableThemes.find((theme) => theme.metadata.id === darkThemeId) ?? fallbackDarkTheme,
|
||||
[availableThemes, darkThemeId, fallbackDarkTheme],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
ensurePierreThemeRegistered(lightTheme);
|
||||
ensurePierreThemeRegistered(darkTheme);
|
||||
}, [darkTheme, lightTheme]);
|
||||
|
||||
const currentVariant = themeSystem?.currentTheme.metadata.variant ?? 'light';
|
||||
|
||||
return {
|
||||
pierreTheme: { light: lightTheme.metadata.id, dark: darkTheme.metadata.id },
|
||||
pierreThemeType: currentVariant === 'dark' ? ('dark' as const) : ('light' as const),
|
||||
};
|
||||
};
|
||||
|
||||
// Parse question tool output: "User has answered your questions: "Q1"="A1", "Q2"="A2". You can now..."
|
||||
const parseQuestionOutput = (output: string): Array<{ question: string; answer: string }> | null => {
|
||||
const match = output.match(/^User has answered your questions:\s*(.+?)\.\s*You can now/s);
|
||||
@@ -1138,30 +1102,6 @@ const TaskToolSummary: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
interface DiffPreviewProps {
|
||||
diff: string;
|
||||
pierreTheme: { light: string; dark: string };
|
||||
pierreThemeType: 'light' | 'dark';
|
||||
diffViewMode: DiffViewMode;
|
||||
}
|
||||
|
||||
const TOOL_DIFF_UNSAFE_CSS = `
|
||||
[data-diff-header],
|
||||
[data-diff] {
|
||||
[data-separator] {
|
||||
height: 24px !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const TOOL_DIFF_METRICS = {
|
||||
hunkLineCount: 50,
|
||||
lineHeight: 24,
|
||||
diffHeaderHeight: 44,
|
||||
hunkSeparatorHeight: 24,
|
||||
spacing: 0,
|
||||
};
|
||||
|
||||
const TOOL_COLLAPSED_CUSTOM_STYLE: React.CSSProperties = {
|
||||
...toolDisplayStyles.getCollapsedStyles(),
|
||||
padding: 0,
|
||||
@@ -1260,85 +1200,18 @@ const renderAnimatedPathWithIcon = (path: string, animate = true, grow = true, s
|
||||
);
|
||||
};
|
||||
|
||||
const PlainDiffFallback: React.FC<{ diff: string }> = ({ diff }) => (
|
||||
<pre
|
||||
className="m-0 overflow-auto whitespace-pre-wrap break-words rounded-lg p-2 typography-code"
|
||||
style={{
|
||||
backgroundColor: 'var(--syntax-base-background)',
|
||||
color: 'var(--syntax-base-foreground)',
|
||||
}}
|
||||
>
|
||||
{diff}
|
||||
</pre>
|
||||
// The rich diff preview is the only tool-card piece that needs the
|
||||
// @pierre/diffs + Shiki stack; lazy-loading it keeps that stack out of the
|
||||
// eager chat graph. While the chunk loads, the plain-text patch renders as the
|
||||
// Suspense fallback, mirroring the preview's own error fallback.
|
||||
const LazyToolPartDiffPreview = lazyWithChunkRecovery(() => import('./ToolPartDiffPreview'));
|
||||
|
||||
const DiffPreview: React.FC<{ diff: string; diffViewMode: DiffViewMode }> = ({ diff, diffViewMode }) => (
|
||||
<React.Suspense fallback={<PlainDiffFallback diff={diff} />}>
|
||||
<LazyToolPartDiffPreview diff={diff} diffViewMode={diffViewMode} />
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
class DiffPreviewErrorBoundary extends React.Component<{
|
||||
resetKey: string;
|
||||
fallback: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}, { hasError: boolean }> {
|
||||
state = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError(): { hasError: boolean } {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: { resetKey: string }) {
|
||||
if (prevProps.resetKey !== this.props.resetKey && this.state.hasError) {
|
||||
this.setState({ hasError: false });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn('Tool diff preview failed; rendering raw patch instead.', error);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, pierreTheme, pierreThemeType, diffViewMode }) => {
|
||||
const options = React.useMemo(
|
||||
() => ({
|
||||
diffStyle: diffViewMode === 'side-by-side' ? 'split' as const : 'unified' as const,
|
||||
diffIndicators: 'none' as const,
|
||||
hunkSeparators: 'line-info-basic' as const,
|
||||
lineDiffType: 'none' as const,
|
||||
disableFileHeader: true,
|
||||
maxLineDiffLength: 1000,
|
||||
expansionLineCount: 20,
|
||||
overflow: 'wrap' as const,
|
||||
theme: pierreTheme,
|
||||
themeType: pierreThemeType,
|
||||
unsafeCSS: TOOL_DIFF_UNSAFE_CSS,
|
||||
}),
|
||||
[diffViewMode, pierreTheme, pierreThemeType]
|
||||
);
|
||||
|
||||
const fallback = <PlainDiffFallback diff={diff} />;
|
||||
|
||||
return (
|
||||
<div className="typography-code px-1 pb-1 pt-0">
|
||||
<DiffPreviewErrorBoundary resetKey={diff} fallback={fallback}>
|
||||
<PatchDiff
|
||||
patch={diff}
|
||||
metrics={TOOL_DIFF_METRICS}
|
||||
options={options}
|
||||
className="block w-full"
|
||||
/>
|
||||
</DiffPreviewErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
DiffPreview.displayName = 'DiffPreview';
|
||||
|
||||
interface ToolExpandedContentProps {
|
||||
part: ToolPartType;
|
||||
state: ToolStateUnion;
|
||||
@@ -1357,7 +1230,6 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
const { t } = useI18n();
|
||||
const runtime = React.useContext(RuntimeAPIContext);
|
||||
const mobileActions = useMobileAppActions();
|
||||
const { pierreTheme, pierreThemeType } = usePierreThemeConfig();
|
||||
const [diffViewMode, setDiffViewMode] = React.useState<DiffViewMode>('unified');
|
||||
const stateWithData = state as ToolStateWithMetadata;
|
||||
const metadata = stateWithData.metadata;
|
||||
@@ -1633,8 +1505,6 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
{entry.renderMode === 'diff' ? (
|
||||
<DiffPreview
|
||||
diff={entry.patch}
|
||||
pierreTheme={pierreTheme}
|
||||
pierreThemeType={pierreThemeType}
|
||||
diffViewMode={diffViewMode}
|
||||
/>
|
||||
) : (
|
||||
@@ -1753,8 +1623,6 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
||||
) : isWriteLikeTool && writeLikeInputPatch ? (
|
||||
<DiffPreview
|
||||
diff={writeLikeInputPatch}
|
||||
pierreTheme={pierreTheme}
|
||||
pierreThemeType={pierreThemeType}
|
||||
diffViewMode={diffViewMode}
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user