mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-13 06:31:47 +00:00
* docs: design frontend performance remediation * docs: plan frontend performance remediation * test(frontend): add route asset performance budgets * perf(nginx): compress textual responses safely * perf(frontend): lazy load case study media * perf(frontend): bound static demo file tracing * perf(frontend): restore static locale boundaries * perf(frontend): defer closed workspace panels * perf(frontend): split editors and deduplicate highlighting * perf(frontend): index incremental message derivation * perf(frontend): stabilize paged history cache policy * perf(frontend): bound streaming markdown renders * perf(frontend): virtualize message history * perf(frontend): bound and virtualize chat lists * perf(frontend): suspend inactive decorative animation * perf(browser): stream latest frames as binary * perf(artifacts): stream bounded text previews * docs: finalize performance runtime boundaries * style(backend): apply test formatting * fix(frontend): keep translation functions client-side * perf(frontend): defer decorative animation bundles * test(frontend): lock optimized route budgets * fix: harden frontend performance boundaries * test(frontend): update i18n provider fixture * fix(frontend): preserve sidebar pagination position * style(backend): format artifact range test
103 lines
2.5 KiB
TypeScript
103 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
import {
|
|
useCallback,
|
|
useLayoutEffect,
|
|
useRef,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
|
|
import type { AgentThread } from "@/core/threads/types";
|
|
|
|
const VIRTUALIZATION_THRESHOLD = 60;
|
|
|
|
export function calculateScrollMargin(
|
|
rootTop: number,
|
|
scrollParentTop: number,
|
|
scrollTop: number,
|
|
) {
|
|
return Math.max(0, rootTop - scrollParentTop + scrollTop);
|
|
}
|
|
|
|
export function VirtualThreadList({
|
|
estimateSize,
|
|
gap = 0,
|
|
items,
|
|
renderItem,
|
|
scrollParentSelector,
|
|
}: {
|
|
estimateSize: number;
|
|
gap?: number;
|
|
items: readonly AgentThread[];
|
|
renderItem: (thread: AgentThread, index: number) => ReactNode;
|
|
scrollParentSelector: string;
|
|
}) {
|
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
const getScrollElement = useCallback(
|
|
() => rootRef.current?.closest<HTMLElement>(scrollParentSelector) ?? null,
|
|
[scrollParentSelector],
|
|
);
|
|
const [scrollMargin, setScrollMargin] = useState(0);
|
|
useLayoutEffect(() => {
|
|
const root = rootRef.current;
|
|
const scrollParent = getScrollElement();
|
|
if (!root || !scrollParent) return;
|
|
setScrollMargin(
|
|
calculateScrollMargin(
|
|
root.getBoundingClientRect().top,
|
|
scrollParent.getBoundingClientRect().top,
|
|
scrollParent.scrollTop,
|
|
),
|
|
);
|
|
}, [getScrollElement, items.length]);
|
|
const virtualizer = useVirtualizer({
|
|
count: items.length,
|
|
estimateSize: () => estimateSize,
|
|
getItemKey: (index) => items[index]?.thread_id ?? index,
|
|
getScrollElement,
|
|
overscan: 8,
|
|
scrollMargin,
|
|
});
|
|
|
|
if (items.length < VIRTUALIZATION_THRESHOLD) {
|
|
return (
|
|
<div
|
|
ref={rootRef}
|
|
className="flex w-full flex-col"
|
|
style={{ gap: `${gap}px` }}
|
|
>
|
|
{items.map(renderItem)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={rootRef}
|
|
className="relative w-full"
|
|
style={{ height: `${virtualizer.getTotalSize()}px` }}
|
|
>
|
|
{virtualizer.getVirtualItems().map((virtualRow) => {
|
|
const thread = items[virtualRow.index];
|
|
if (!thread) return null;
|
|
return (
|
|
<div
|
|
key={virtualRow.key}
|
|
ref={virtualizer.measureElement}
|
|
data-index={virtualRow.index}
|
|
className="absolute top-0 left-0 w-full"
|
|
style={{
|
|
paddingBottom: `${gap}px`,
|
|
transform: `translateY(${virtualRow.start - scrollMargin}px)`,
|
|
}}
|
|
>
|
|
{renderItem(thread, virtualRow.index)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|