perf(ui): enable session timeline virtualization and defer heavy tool content

Virtualize message timeline at 5+ turns. Stabilize virtualizer keys,
add prepend-aware scroll compensation for "Load older", and defer
expanded tool bodies by one animation frame. Tighten initial turn
window to 7 turns for faster session open.
This commit is contained in:
Bohdan Triapitsyn
2026-05-12 14:44:53 +03:00
parent 644050824e
commit d587ed7542
4 changed files with 105 additions and 59 deletions
@@ -247,32 +247,6 @@ export const useChatTimelineController = ({
anchor: ViewportAnchor | null;
} | null>(null);
React.useLayoutEffect(() => {
const snap = prePrependScrollRef.current;
const container = scrollRef.current;
if (!snap || !container) return;
prePrependScrollRef.current = null;
// Try anchor-based restoration first (pixel-perfect)
if (snap.anchor) {
const anchorEl = container.querySelector<HTMLElement>(
`[data-message-id="${snap.anchor.messageId}"]`,
);
if (anchorEl) {
const containerRect = container.getBoundingClientRect();
const anchorTop = anchorEl.getBoundingClientRect().top - containerRect.top;
container.scrollTop += anchorTop - snap.anchor.offsetTop;
return;
}
}
// Fallback: height-delta compensation
const delta = container.scrollHeight - snap.height;
if (delta > 0) {
container.scrollTop = snap.top + delta;
}
}, [renderedMessages, scrollRef]);
const captureViewportAnchor = React.useCallback((): ViewportAnchor | null => {
return messageListRef.current?.captureViewportAnchor() ?? null;
}, [messageListRef]);
@@ -281,6 +255,26 @@ export const useChatTimelineController = ({
return messageListRef.current?.restoreViewportAnchor(anchor) ?? false;
}, [messageListRef]);
React.useLayoutEffect(() => {
const snap = prePrependScrollRef.current;
const container = scrollRef.current;
if (!snap || !container) return;
prePrependScrollRef.current = null;
// When a viewport anchor is available, delegate to MessageList
// restoreViewportAnchor which falls back to virtualizer-aware
// scrollHistoryIndexIntoView when the element is not in the DOM.
if (snap.anchor && restoreViewportAnchor(snap.anchor)) {
return;
}
// Fallback: height-delta compensation
const delta = container.scrollHeight - snap.height;
if (delta > 0) {
container.scrollTop = snap.top + delta;
}
}, [renderedMessages, scrollRef, restoreViewportAnchor]);
const revealBufferedTurns = React.useCallback(async (): Promise<boolean> => {
if (turnStartRef.current <= 0 || pendingRevealWorkRef.current) {
return false;