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
@@ -23,6 +23,7 @@ import {
} from './toolRenderers';
import type { ToolPopupContent, DiffViewMode } from './types';
import { DiffViewToggle } from './DiffViewToggle';
import { VirtualizedCodeBlock, type CodeLine } from './parts/VirtualizedCodeBlock';
interface ToolOutputDialogProps {
popup: ToolPopupContent;
@@ -355,6 +356,104 @@ const ImagePreviewDialog: React.FC<{
return createPortal(content, document.body);
};
// ── PERF-007: Virtualised sub-components for dialog ──────────────────
const DialogUnifiedDiff: React.FC<{
popup: ToolPopupContent;
syntaxTheme: Record<string, React.CSSProperties>;
isMobile: boolean;
}> = React.memo(({ popup, syntaxTheme, isMobile }) => {
const hunks = React.useMemo(() => parseDiffToUnified(popup.content), [popup.content]);
return (
<div className="typography-code">
{hunks.map((hunk, hunkIdx) => {
const inputFile = (typeof popup.metadata?.input === 'object' && popup.metadata.input !== null)
? ((popup.metadata.input as Record<string, unknown>).file_path || (popup.metadata.input as Record<string, unknown>).filePath)
: null;
const fileStr = typeof inputFile === 'string' ? inputFile : '';
const hunkFileStr = typeof hunk.file === 'string' ? hunk.file : '';
const lang = getLanguageFromExtension(fileStr || hunkFileStr || '') || '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="border-b border-border/20 last:border-b-0">
<div className={cn('bg-muted/20 px-3 py-2 font-medium text-muted-foreground border-b border-border/10 sticky top-0 z-10 break-words -mx-3', isMobile ? 'typography-micro' : 'typography-markdown')}>
{`${hunk.file} (line ${hunk.oldStart})`}
</div>
<VirtualizedCodeBlock
lines={codeLines}
language={lang}
syntaxTheme={syntaxTheme}
maxHeight="70vh"
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>
);
});
DialogUnifiedDiff.displayName = 'DialogUnifiedDiff';
const DialogReadContent: React.FC<{
popup: ToolPopupContent;
syntaxTheme: Record<string, React.CSSProperties>;
}> = React.memo(({ popup, syntaxTheme }) => {
const parsedReadOutput = React.useMemo(() => parseReadToolOutput(popup.content), [popup.content]);
const codeLines: CodeLine[] = React.useMemo(() => {
const inputMeta = popup.metadata?.input;
const inputObj = typeof inputMeta === 'object' && inputMeta !== null ? (inputMeta as Record<string, unknown>) : {};
const offset = typeof inputObj.offset === 'number' ? inputObj.offset : 0;
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, popup.metadata?.input]);
return (
<VirtualizedCodeBlock
lines={codeLines}
language={popup.language || 'text'}
syntaxTheme={syntaxTheme}
maxHeight="70vh"
/>
);
});
DialogReadContent.displayName = 'DialogReadContent';
const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange, syntaxTheme, isMobile }) => {
const [diffViewMode, setDiffViewMode] = React.useState<DiffViewMode>(isMobile ? 'unified' : 'side-by-side');
@@ -460,82 +559,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
{popup.isDiff ? (
diffViewMode === 'unified' ? (
<div className="typography-code">
{parseDiffToUnified(popup.content).map((hunk, hunkIdx) => (
<div key={hunkIdx} className="border-b border-border/20 last:border-b-0">
<div
className={cn('bg-muted/20 px-3 py-2 font-medium text-muted-foreground border-b border-border/10 sticky top-0 z-10 break-words -mx-3', isMobile ? 'typography-micro' : 'typography-markdown')}
>
{`${hunk.file} (line ${hunk.oldStart})`}
</div>
<div>
{hunk.lines.map((line, lineIdx) => (
<div
key={lineIdx}
className={cn(
'typography-code font-mono px-3 py-0.5 flex',
line.type === 'context' && 'bg-transparent',
line.type === 'removed' && 'bg-transparent',
line.type === 'added' && 'bg-transparent'
)}
style={{
lineHeight: '1.1',
...(line.type === 'removed'
? { backgroundColor: 'var(--tools-edit-removed-bg)' }
: line.type === 'added'
? { backgroundColor: 'var(--tools-edit-added-bg)' }
: {}),
}}
>
<span className="w-12 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">
{(() => {
const inputFile = (typeof popup.metadata?.input === 'object' && popup.metadata.input !== null)
? ((popup.metadata.input as Record<string, unknown>).file_path || (popup.metadata.input as Record<string, unknown>).filePath)
: null;
const fileStr = typeof inputFile === 'string' ? inputFile : '';
const hunkFile = (typeof hunk === 'object' && hunk !== null && 'file' in hunk) ? (hunk as UnifiedDiffHunk).file : null;
const hunkFileStr = typeof hunkFile === 'string' ? hunkFile : '';
const finalFile = fileStr || hunkFileStr || '';
return (
<SyntaxHighlighter
style={syntaxTheme}
language={getLanguageFromExtension(finalFile) || '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',
},
}}
>
{line.content}
</SyntaxHighlighter>
);
})()}
</div>
</div>
))}
</div>
</div>
))}
</div>
<DialogUnifiedDiff popup={popup} syntaxTheme={syntaxTheme} isMobile={isMobile} />
) : popup.diffHunks ? (
<div className="typography-code">
{popup.diffHunks.map((hunk, hunkIdx) => (
@@ -741,77 +765,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
}
if (tool === 'read') {
const parsedReadOutput = parseReadToolOutput(popup.content);
const inputMeta = popup.metadata?.input;
const inputObj = typeof inputMeta === 'object' && inputMeta !== null ? (inputMeta as Record<string, unknown>) : {};
const offset = typeof inputObj.offset === 'number' ? inputObj.offset : 0;
let fallbackLineCursor = offset;
const hasExplicitLineNumbers = parsedReadOutput.lines.some((line) => line.lineNumber !== null);
return (
<div>
{parsedReadOutput.lines.map((line, idx: number) => {
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={`typography-code font-mono flex ${line.isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
<span className="w-12 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={popup.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.text}
</SyntaxHighlighter>
)}
</div>
</div>
);
})}
</div>
);
return <DialogReadContent popup={popup} syntaxTheme={syntaxTheme} />;
}
return (