perf: fix streaming lag, memory leaks, stuck spinners, and proxy timeout (#483)

* perf: fix streaming lag, memory leaks, and proxy timeout

- PERF-001: Batch all streaming parts via requestAnimationFrame instead of
  per-token Zustand set() calls (~100/sec → 1 per frame)
- PERF-002: Fix direct state.sessionMemoryState mutation inside set() callback
- PERF-003: Debounce messageStore→sessionStore subscription via rAF + 500ms
  title computation delay
- PERF-004: Stabilize SSE callbacks with refs to prevent reconnection storms;
  add 5-min stuck session idle timeout
- PERF-005: Bound messageCache (500 max, LRU eviction), cap registry Maps,
  cleanup on session eviction
- PERF-006: Replace toast duration: Infinity with 30s + id-based dedup
- Fix proxy timeout: POST /session/:id/message 45s → 4min (matches CLI)
- Add vitest + jsdom test infrastructure (61 tests across 7 files)

Addresses: #476 (stuck spinner), #358 (34GB memory leak), #190 (browser lag)

* perf: virtualize tool output rendering (read, edit, write)

- PERF-007: Replace per-line <SyntaxHighlighter> with VirtualizedCodeBlock:
  - ONE Prism.highlight() call for entire file instead of N per-line calls
  - @tanstack/react-virtual renders only visible rows (~30 vs 2000+)
  - Applied to: ToolPart (read, DiffPreview, WriteInputPreview) and
    ToolOutputDialog (unified diff, read content)
- PERF-008: Memoize parseReadToolOutput/parseDiffToUnified via useMemo
  to prevent re-parsing on every re-render

A 2000-line file read now mounts ~30 DOM nodes instead of 2000
SyntaxHighlighter instances, eliminating the main-thread blocking
that caused UI freezes during file operations.

* fix: resolve lint errors (unused vars in tests and VirtualizedCodeBlock)

* chore: trim PR scope to core perf fixes

* fix: restore tool-card highlight stability

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Nguyễn Ngô Thượng
2026-02-23 12:05:53 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 989593ed72
commit 3cd6d051cb
11 changed files with 756 additions and 407 deletions
@@ -27,6 +27,7 @@ import {
formatInputForDisplay,
parseReadToolOutput,
} from '../toolRenderers';
import { VirtualizedCodeBlock, type CodeLine } from './VirtualizedCodeBlock';
type ToolStateWithMetadata = ToolStateUnion & { metadata?: Record<string, unknown>; input?: Record<string, unknown>; output?: string; error?: string; time?: { start: number; end?: number } };
@@ -570,74 +571,48 @@ interface DiffPreviewProps {
input?: ToolStateWithMetadata['input'];
}
const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme, input }) => (
<div className="typography-code px-1 pb-1 pt-0 space-y-0">
{parseDiffToUnified(diff).map((hunk, hunkIdx) => (
<div key={hunkIdx} className="-mx-1 px-1 last:border-b-0" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground break-words -mx-1" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
{`${hunk.file} (line ${hunk.oldStart})`}
</div>
const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme, input }) => {
const hunks = React.useMemo(() => parseDiffToUnified(diff), [diff]);
<div>
{hunk.lines.map((line, lineIdx) => (
<div
key={lineIdx}
className={cn(
'typography-code font-mono px-2 py-0.5 flex -mx-2',
line.type === 'context' && 'bg-transparent',
line.type === 'removed' && 'bg-transparent',
line.type === 'added' && 'bg-transparent'
)}
style={
line.type === 'removed'
? { backgroundColor: 'var(--tools-edit-removed-bg)' }
: line.type === 'added'
? { backgroundColor: 'var(--tools-edit-added-bg)' }
: {}
}
>
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{line.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
<SyntaxHighlighter
style={syntaxTheme}
language={getLanguageFromExtension(typeof input?.file_path === 'string' ? input.file_path : typeof input?.filePath === 'string' ? input.filePath : hunk.file) || 'text'}
PreTag="div"
wrapLines
wrapLongLines
customStyle={{
margin: 0,
padding: 0,
fontSize: 'inherit',
background: 'transparent',
backgroundColor: 'transparent',
borderRadius: 0,
overflow: 'visible',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
overflowWrap: 'anywhere',
color: line.type === 'removed' ? 'var(--tools-edit-removed)' : line.type === 'added' ? 'var(--tools-edit-added)' : 'inherit',
}}
codeTagProps={{
style: {
background: 'transparent',
backgroundColor: 'transparent',
fontSize: 'inherit',
color: line.type === 'removed' ? 'var(--tools-edit-removed)' : line.type === 'added' ? 'var(--tools-edit-added)' : 'inherit',
},
}}
>
{line.content}
</SyntaxHighlighter>
</div>
return (
<div className="typography-code px-1 pb-1 pt-0 space-y-0">
{hunks.map((hunk, hunkIdx) => {
const lang = getLanguageFromExtension(
typeof input?.file_path === 'string' ? input.file_path
: typeof input?.filePath === 'string' ? input.filePath
: hunk.file
) || 'text';
const codeLines: CodeLine[] = hunk.lines.map((line) => ({
text: line.content,
lineNumber: line.lineNumber || null,
type: line.type as CodeLine['type'],
}));
return (
<div key={hunkIdx} className="-mx-1 px-1 last:border-b-0" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground break-words -mx-1" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
{`${hunk.file} (line ${hunk.oldStart})`}
</div>
))}
</div>
</div>
))}
</div>
));
<VirtualizedCodeBlock
lines={codeLines}
language={lang}
syntaxTheme={syntaxTheme}
maxHeight="50vh"
lineStyles={(line) =>
line.type === 'removed'
? { backgroundColor: 'var(--tools-edit-removed-bg)', color: 'var(--tools-edit-removed)' }
: line.type === 'added'
? { backgroundColor: 'var(--tools-edit-added-bg)', color: 'var(--tools-edit-added)' }
: undefined
}
/>
</div>
);
})}
</div>
);
});
DiffPreview.displayName = 'DiffPreview';
@@ -649,13 +624,20 @@ interface WriteInputPreviewProps {
}
const WriteInputPreview: React.FC<WriteInputPreviewProps> = React.memo(({ content, syntaxTheme, filePath, displayPath }) => {
const lines = React.useMemo(() => content.split('\n'), [content]);
const language = React.useMemo(
() => getLanguageFromExtension(filePath ?? '') || detectLanguageFromOutput(content, 'write', filePath ? { filePath } : undefined),
[content, filePath]
);
const lineCount = Math.max(lines.length, 1);
const codeLines: CodeLine[] = React.useMemo(() => {
const rawLines = content.split('\n');
return rawLines.map((text, idx) => ({
text: text || ' ',
lineNumber: idx + 1,
}));
}, [content]);
const lineCount = Math.max(codeLines.length, 1);
const headerLineLabel = lineCount === 1 ? 'line 1' : `lines 1-${lineCount}`;
return (
@@ -663,47 +645,83 @@ const WriteInputPreview: React.FC<WriteInputPreviewProps> = React.memo(({ conten
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground rounded-lg mb-1" style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}>
{`${displayPath} (${headerLineLabel})`}
</div>
<div className="space-y-0">
{lines.map((line, lineIdx) => (
<div key={lineIdx} className="typography-code font-mono px-2 py-0.5 flex -mx-1">
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{lineIdx + 1}
</span>
<div className="flex-1 min-w-0">
<SyntaxHighlighter
style={syntaxTheme}
language={language || 'text'}
PreTag="div"
wrapLines
wrapLongLines
customStyle={{
margin: 0,
padding: 0,
fontSize: 'inherit',
background: 'transparent',
backgroundColor: 'transparent',
borderRadius: 0,
overflow: 'visible',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
overflowWrap: 'anywhere',
}}
codeTagProps={{
style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' },
}}
>
{line || ' '}
</SyntaxHighlighter>
</div>
</div>
))}
</div>
<VirtualizedCodeBlock
lines={codeLines}
language={language || 'text'}
syntaxTheme={syntaxTheme}
maxHeight="50vh"
/>
</div>
);
});
WriteInputPreview.displayName = 'WriteInputPreview';
// ── PERF-007: Read tool output with virtualised highlighting ─────────
interface ReadToolVirtualizedProps {
outputString: string;
input?: Record<string, unknown>;
syntaxTheme: { [key: string]: React.CSSProperties };
toolName: string;
renderScrollableBlock: (
content: React.ReactNode,
options?: { maxHeightClass?: string; className?: string; disableHorizontal?: boolean; outerClassName?: string }
) => React.ReactNode;
}
const ReadToolVirtualized: React.FC<ReadToolVirtualizedProps> = React.memo(({
outputString,
input,
syntaxTheme,
toolName,
renderScrollableBlock,
}) => {
const parsedReadOutput = React.useMemo(() => parseReadToolOutput(outputString), [outputString]);
const offset = typeof input?.offset === 'number' ? input.offset : 0;
const codeLines: CodeLine[] = React.useMemo(() => {
const hasExplicitLineNumbers = parsedReadOutput.lines.some((line) => line.lineNumber !== null);
let fallbackLineCursor = offset;
return parsedReadOutput.lines.map((line) => {
if (line.lineNumber !== null) {
fallbackLineCursor = line.lineNumber;
}
const shouldAssignFallback =
parsedReadOutput.type === 'file'
&& !hasExplicitLineNumbers
&& line.lineNumber === null
&& !line.isInfo;
const effectiveLineNumber = line.lineNumber ?? (shouldAssignFallback
? (fallbackLineCursor += 1)
: null);
return {
text: line.text,
lineNumber: effectiveLineNumber,
isInfo: line.isInfo,
};
});
}, [parsedReadOutput, offset]);
const language = React.useMemo(() => {
const contentForLanguage = parsedReadOutput.lines.map((l) => l.text).join('\n');
return detectLanguageFromOutput(contentForLanguage, toolName, input as Record<string, unknown>);
}, [parsedReadOutput, toolName, input]);
return renderScrollableBlock(
<VirtualizedCodeBlock
lines={codeLines}
language={language}
syntaxTheme={syntaxTheme}
maxHeight="55vh"
/>,
{ className: 'p-1' }
) as React.ReactElement;
});
ReadToolVirtualized.displayName = 'ReadToolVirtualized';
interface ImagePreviewProps {
content: string;
filePath: string;
@@ -969,72 +987,13 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
if (hasStringOutput && outputString.trim()) {
if (part.tool === 'read') {
const parsedReadOutput = parseReadToolOutput(outputString);
const offset = typeof input?.offset === 'number' ? input.offset : 0;
const contentForLanguage = parsedReadOutput.lines.map((line) => line.text).join('\n');
let fallbackLineCursor = offset;
const hasExplicitLineNumbers = parsedReadOutput.lines.some((line) => line.lineNumber !== null);
return renderScrollableBlock(
<div className="typography-code w-full min-w-0 space-y-1">
{parsedReadOutput.lines.map((line, idx) => {
if (line.lineNumber !== null) {
fallbackLineCursor = line.lineNumber;
}
const shouldAssignFallbackLineNumber =
parsedReadOutput.type === 'file'
&& !hasExplicitLineNumbers
&& line.lineNumber === null
&& !line.isInfo;
const effectiveLineNumber = line.lineNumber ?? (shouldAssignFallbackLineNumber
? (fallbackLineCursor += 1)
: null);
const shouldShowLineNumber = !line.isInfo && effectiveLineNumber !== null;
return (
<div key={idx} className={cn('typography-code font-mono flex w-full min-w-0', line.isInfo && 'text-muted-foreground/70 italic')}>
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{shouldShowLineNumber ? effectiveLineNumber : ''}
</span>
<div className="flex-1 min-w-0">
{line.isInfo ? (
<div className="whitespace-pre-wrap break-words">{line.text}</div>
) : (
<SyntaxHighlighter
style={syntaxTheme}
language={detectLanguageFromOutput(contentForLanguage, part.tool, input as Record<string, unknown>)}
PreTag="div"
wrapLines
wrapLongLines
customStyle={{
margin: 0,
padding: 0,
fontSize: 'inherit',
background: 'transparent',
backgroundColor: 'transparent',
borderRadius: 0,
overflow: 'visible',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
overflowWrap: 'anywhere',
}}
codeTagProps={{
style: {
background: 'transparent',
backgroundColor: 'transparent',
},
}}
>
{line.text}
</SyntaxHighlighter>
)}
</div>
</div>
);
})}
</div>,
{ className: 'p-1' }
);
return <ReadToolVirtualized
outputString={outputString}
input={input}
syntaxTheme={syntaxTheme}
toolName={part.tool}
renderScrollableBlock={renderScrollableBlock}
/>;
}
return renderScrollableBlock(