perf: migrate chat rendering to virtua (#1651)

* refactor: migrate chat history virtualization to virtua

* refactor: render loaded chat history directly

* refactor: finish virtua migration

* perf: defer tool body rendering

* perf: queue deferred tool body mounts

* perf: quiet and defer markdown file probes

* perf: defer markdown code highlighting

* perf: stabilize markdown plugin lists

* perf: defer mermaid markdown rendering

* perf: delay markdown file reference annotation

* perf: attach markdown table listeners on demand

* perf: trim markdown render overhead
This commit is contained in:
Bohdan Triapitsyn
2026-06-15 03:29:40 +03:00
committed by GitHub
parent e372c8d8cb
commit a45376d585
24 changed files with 621 additions and 671 deletions
@@ -1,5 +1,5 @@
import React from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Virtualizer, type VirtualizerHandle } from 'virtua';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -195,30 +195,16 @@ export const ChangesPanel: React.FC<ChangesPanelProps> = ({
const rowCount = rows.length;
const shouldVirtualize = rowCount >= CHANGE_LIST_VIRTUALIZE_THRESHOLD;
const rowVirtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => scrollRef.current,
estimateSize: () => CHANGE_ROW_ESTIMATE_PX,
overscan: 12,
enabled: shouldVirtualize,
});
const rowVirtualizerRef = React.useRef<VirtualizerHandle | null>(null);
const [visibleStartIndex, setVisibleStartIndex] = React.useState(0);
// Remeasure when the container transitions from display:none (hidden tab) back
// to visible layout, otherwise stale zero-height measurements render no rows.
React.useEffect(() => {
if (!shouldVirtualize) return;
const el = scrollRef.current;
if (!el) return;
const observer = new ResizeObserver(() => rowVirtualizer.measure());
observer.observe(el);
return () => observer.disconnect();
}, [shouldVirtualize, rowVirtualizer]);
const totalSize = rowVirtualizer.getTotalSize();
const virtualRows = React.useMemo(
() => (shouldVirtualize && totalSize >= 0 ? rowVirtualizer.getVirtualItems() : []),
[shouldVirtualize, rowVirtualizer, totalSize]
);
const updateVisibleStartIndex = React.useCallback((offset: number) => {
const virtualizer = rowVirtualizerRef.current;
const next = virtualizer
? virtualizer.findItemIndex(offset)
: Math.floor(offset / CHANGE_ROW_ESTIMATE_PX);
setVisibleStartIndex((previous) => (previous === next ? previous : next));
}, []);
React.useEffect(() => {
if (!onVisiblePathsChange) {
@@ -246,11 +232,16 @@ export const ChangesPanel: React.FC<ChangesPanelProps> = ({
}
onVisiblePathsChange(
virtualRows
.map((item) => collectFromRow(rows[item.index]))
rows
.slice(
visibleStartIndex,
visibleStartIndex + Math.ceil((scrollRef.current?.clientHeight ?? 0) / CHANGE_ROW_ESTIMATE_PX) + VISIBLE_PREFETCH_LIMIT
)
.map((row) => collectFromRow(row))
.filter((value): value is string => Boolean(value))
.slice(0, VISIBLE_PREFETCH_LIMIT)
);
}, [onVisiblePathsChange, rowCount, rows, shouldVirtualize, virtualRows]);
}, [onVisiblePathsChange, rowCount, rows, shouldVirtualize, visibleStartIndex]);
const toggleGroupCollapsed = React.useCallback((groupId: string) => {
setCollapsedGroups((previous) => {
@@ -490,27 +481,27 @@ export const ChangesPanel: React.FC<ChangesPanelProps> = ({
className="overlay-scrollbar-target overlay-scrollbar-container min-h-0 w-full flex-1 overflow-x-hidden overflow-y-auto"
>
{shouldVirtualize ? (
<div className="relative w-full" style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
{virtualRows.map((item) => {
const row = rows[item.index];
if (!row) return null;
return (
<div
key={row.key}
ref={rowVirtualizer.measureElement}
data-index={item.index}
className={cn(
'absolute left-0 top-0 w-full',
showDivider(item.index) &&
'before:pointer-events-none before:absolute before:left-0 before:right-2 before:top-0 before:border-t before:border-border/60'
)}
style={{ transform: `translateY(${item.start}px)` }}
>
{renderRow(row, item.index === 0)}
</div>
);
})}
</div>
<Virtualizer
ref={rowVirtualizerRef}
data={rows}
itemSize={CHANGE_ROW_ESTIMATE_PX}
bufferSize={CHANGE_ROW_ESTIMATE_PX * 12}
scrollRef={scrollRef}
onScroll={updateVisibleStartIndex}
>
{(row, index) => (
<div
key={row.key}
className={cn(
'relative',
showDivider(index) &&
'before:pointer-events-none before:absolute before:left-0 before:right-2 before:top-0 before:border-t before:border-border/60'
)}
>
{renderRow(row, index === 0)}
</div>
)}
</Virtualizer>
) : (
<div role="list" aria-label={t('gitView.changes.changedFilesAria')}>
{rows.map((row, index) => (
@@ -1527,6 +1527,7 @@ export const PullRequestSection: React.FC<{
<SimpleMarkdownRenderer
content={pr.body}
className="typography-markdown-body text-muted-foreground break-words mt-1"
enableFileReferences={false}
/>
) : (
<div className="typography-micro text-muted-foreground whitespace-pre-wrap break-words mt-1">
@@ -2031,6 +2032,7 @@ export const PullRequestSection: React.FC<{
'typography-markdown-body text-foreground break-words [&_a]:no-underline [&_a:hover]:no-underline',
selfMentionHighlightClass,
].filter(Boolean).join(' ')}
enableFileReferences={false}
/>
</div>
</div>