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,12 +1,12 @@
import React from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Virtualizer } from 'virtua';
import type { Session } from '@opencode-ai/sdk/v2';
// Archived buckets routinely grow into the hundreds/thousands; virtualize
// when we cross this row count so the DOM stays bounded.
const ARCHIVED_VIRTUALIZE_THRESHOLD = 50;
// Compact rows in the archived bucket without nested subagents render
// around 24-32px; tanstack-virtual will measure precisely via the row ref.
// around 24-32px; virtua measures mounted rows and uses this as the initial hint.
const ARCHIVED_ROW_ESTIMATE_PX = 28;
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
@@ -274,9 +274,10 @@ export function SessionGroupSection(props: Props): React.ReactNode {
&& visibleSessions.length >= ARCHIVED_VIRTUALIZE_THRESHOLD;
const archivedVirtualContainerRef = React.useRef<HTMLDivElement | null>(null);
const archivedScrollRef = React.useRef<HTMLElement | null>(null);
const [archivedScrollEl, setArchivedScrollEl] = React.useState<HTMLElement | null>(null);
// Offset of the virtual container from the scroll element's content origin.
// tanstack-virtual reads scrollMargin from useVirtualizer options and uses it
// virtua reads startMargin from Virtualizer options and uses it
// to translate scrollTop into container-relative coordinates. Without this,
// when the scroll element is an ancestor (the sidebar's ScrollableOverlay),
// the virtualizer assumes the container starts at the top of the scroll
@@ -287,15 +288,6 @@ export function SessionGroupSection(props: Props): React.ReactNode {
// routes its scroll through `ScrollableOverlay` higher up the tree;
// threading a ref through every intermediate component would be invasive
// for this single use case.
const archivedVirtualizer = useVirtualizer({
count: visibleSessions.length,
getScrollElement: () => archivedScrollEl,
estimateSize: () => ARCHIVED_ROW_ESTIMATE_PX,
overscan: 8,
enabled: shouldVirtualizeArchived && archivedScrollEl !== null,
scrollMargin: archivedScrollMargin,
});
// Resolve the scrolling ancestor and measure the virtual container's offset
// from its content origin, both on every render. The container ref is null
// while the archived bucket is collapsed (the body isn't mounted), so a
@@ -309,6 +301,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
React.useLayoutEffect(() => {
if (!shouldVirtualizeArchived) {
if (archivedScrollEl !== null) setArchivedScrollEl(null);
archivedScrollRef.current = null;
if (archivedScrollMargin !== 0) setArchivedScrollMargin(0);
return;
}
@@ -332,6 +325,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
el = el.parentElement;
}
if (scrollEl !== archivedScrollEl) {
archivedScrollRef.current = scrollEl;
setArchivedScrollEl(scrollEl);
// setState triggers a re-render; bail out and let the next pass
// measure the margin against the fresh element.
@@ -345,29 +339,6 @@ export function SessionGroupSection(props: Props): React.ReactNode {
setArchivedScrollMargin((prev) => (Math.abs(prev - offset) < 1 ? prev : offset));
});
// Re-measure when the sidebar's scroll container resizes (e.g. window
// resize, sidebar width change). Mirrors the pattern in ChangesSection.
React.useEffect(() => {
if (!shouldVirtualizeArchived || !archivedScrollEl) return;
const observer = new ResizeObserver(() => {
archivedVirtualizer.measure();
});
observer.observe(archivedScrollEl);
return () => observer.disconnect();
}, [shouldVirtualizeArchived, archivedScrollEl, archivedVirtualizer]);
const archivedTotalSize = archivedVirtualizer.getTotalSize();
// Read virtual rows directly in render rather than via useMemo. The
// virtualizer instance is a stable reference across renders, and on a
// pure scroll event neither it nor `archivedTotalSize` change — only the
// virtualizer's internal scroll offset does. Memoizing here would return
// stale rows after every scroll. tanstack-virtual v3 expects callers to
// read getVirtualItems() inline; it's cheap and returns [] when the
// virtualizer is disabled.
const archivedVirtualRows = shouldVirtualizeArchived && archivedScrollEl !== null
? archivedVirtualizer.getVirtualItems()
: [];
if (hasSessionSearchQuery && !groupMatchesSearch && rootFolders.length === 0 && ungroupedSessions.length === 0) {
return null;
}
@@ -576,41 +547,16 @@ export function SessionGroupSection(props: Props): React.ReactNode {
>
{renderFolderItems()}
{shouldVirtualizeArchived ? (
<div
ref={archivedVirtualContainerRef}
style={{
position: 'relative',
// Reserve scroll height for all archived rows so the parent
// scroll thumb reflects the full list. Individual rows are
// absolutely positioned by translateY.
height: archivedTotalSize > 0 ? archivedTotalSize : undefined,
width: '100%',
}}
>
{archivedVirtualRows.map((virtualRow) => {
const node = visibleSessions[virtualRow.index];
if (!node) return null;
return (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={archivedVirtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
// virtualRow.start is in scroll-element coordinates (offset by
// scrollMargin). Subtract scrollMargin to position within the
// container, which itself starts at scrollMargin in the scroll
// element.
transform: `translateY(${virtualRow.start - archivedScrollMargin}px)`,
}}
>
{renderSessionNode(node, 0, group.directory, projectId, true)}
</div>
);
})}
<div ref={archivedVirtualContainerRef}>
<Virtualizer
data={visibleSessions}
itemSize={ARCHIVED_ROW_ESTIMATE_PX}
bufferSize={ARCHIVED_ROW_ESTIMATE_PX * 8}
scrollRef={archivedScrollRef}
startMargin={archivedScrollMargin}
>
{(node) => renderSessionNode(node, 0, group.directory, projectId, true) as React.ReactElement}
</Virtualizer>
</div>
) : (
visibleSessions.map((node) => renderSessionNode(node, 0, group.directory, projectId, group.isArchivedBucket === true))