perf(app): reduce streaming rerenders and scroll churn (#67)

Amp-Thread-ID: https://ampcode.com/threads/T-019c793a-d087-762a-882a-8cc11e6101db

Co-authored-by: Ubuntu <ubuntu@dev-yk.tail388b2e.ts.net>
This commit is contained in:
YK
2026-02-22 13:09:49 +07:00
committed by GitHub
co-authored by Ubuntu
parent 55b450c8b9
commit 8bcfde8184
3 changed files with 107 additions and 19 deletions
@@ -107,6 +107,8 @@ export function AgentStreamView({
const hasScrolledInitially = useRef(false); const hasScrolledInitially = useRef(false);
const hasAutoScrolledOnce = useRef(false); const hasAutoScrolledOnce = useRef(false);
const isNearBottomRef = useRef(true); const isNearBottomRef = useRef(true);
const pendingAutoScrollFrameRef = useRef<number | null>(null);
const pendingAutoScrollAnimatedRef = useRef(false);
const streamItemCountRef = useRef(0); const streamItemCountRef = useRef(0);
const streamScrollbarMetrics = useWebDesktopScrollbarMetrics(); const streamScrollbarMetrics = useWebDesktopScrollbarMetrics();
const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>(new Set()); const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>(new Set());
@@ -214,6 +216,35 @@ export function AgentStreamView({
[] []
); );
const scheduleAutoScroll = useCallback(
({ animated }: { animated: boolean }) => {
pendingAutoScrollAnimatedRef.current =
pendingAutoScrollAnimatedRef.current || animated;
if (pendingAutoScrollFrameRef.current !== null) {
return;
}
pendingAutoScrollFrameRef.current = requestAnimationFrame(() => {
pendingAutoScrollFrameRef.current = null;
const shouldAnimate = pendingAutoScrollAnimatedRef.current;
pendingAutoScrollAnimatedRef.current = false;
scrollToBottomInternal({ animated: shouldAnimate });
});
},
[scrollToBottomInternal]
);
useEffect(() => {
return () => {
if (pendingAutoScrollFrameRef.current !== null) {
cancelAnimationFrame(pendingAutoScrollFrameRef.current);
pendingAutoScrollFrameRef.current = null;
}
pendingAutoScrollAnimatedRef.current = false;
};
}, []);
useEffect(() => { useEffect(() => {
if (streamItems.length === 0) { if (streamItems.length === 0) {
return; return;
@@ -233,9 +264,9 @@ export function AgentStreamView({
} }
const shouldAnimate = hasScrolledInitially.current; const shouldAnimate = hasScrolledInitially.current;
scrollToBottomInternal({ animated: shouldAnimate }); scheduleAutoScroll({ animated: shouldAnimate });
hasScrolledInitially.current = true; hasScrolledInitially.current = true;
}, [streamItems, scrollToBottomInternal]); }, [streamItems, scheduleAutoScroll]);
function scrollToBottom() { function scrollToBottom() {
scrollToBottomInternal({ animated: true }); scrollToBottomInternal({ animated: true });
+8 -17
View File
@@ -183,6 +183,9 @@ function SessionProviderInternal({
const setAgentStreamHead = useSessionStore( const setAgentStreamHead = useSessionStore(
(state) => state.setAgentStreamHead (state) => state.setAgentStreamHead
); );
const setAgentStreamState = useSessionStore(
(state) => state.setAgentStreamState
);
const clearAgentStreamHead = useSessionStore( const clearAgentStreamHead = useSessionStore(
(state) => state.clearAgentStreamHead (state) => state.clearAgentStreamHead
); );
@@ -784,23 +787,10 @@ function SessionProviderInternal({
timestamp: parsedTimestamp, timestamp: parsedTimestamp,
}); });
if (changedTail) { if (changedTail || changedHead) {
setAgentStreamTail(serverId, (prev) => { setAgentStreamState(serverId, agentId, {
const next = new Map(prev); ...(changedTail ? { tail } : {}),
next.set(agentId, tail); ...(changedHead ? { head } : {}),
return next;
});
}
if (changedHead) {
setAgentStreamHead(serverId, (prev) => {
const next = new Map(prev);
if (head.length > 0) {
next.set(agentId, head);
} else {
next.delete(agentId);
}
return next;
}); });
} }
@@ -1261,6 +1251,7 @@ function SessionProviderInternal({
setCurrentAssistantMessage, setCurrentAssistantMessage,
setAgentStreamTail, setAgentStreamTail,
setAgentStreamHead, setAgentStreamHead,
setAgentStreamState,
clearAgentStreamHead, clearAgentStreamHead,
setAgentTimelineCursor, setAgentTimelineCursor,
setInitializingAgents, setInitializingAgents,
+66
View File
@@ -243,6 +243,11 @@ interface SessionStoreActions {
// Stream state (head/tail model) // Stream state (head/tail model)
setAgentStreamTail: (serverId: string, state: Map<string, StreamItem[]> | ((prev: Map<string, StreamItem[]>) => Map<string, StreamItem[]>)) => void; setAgentStreamTail: (serverId: string, state: Map<string, StreamItem[]> | ((prev: Map<string, StreamItem[]>) => Map<string, StreamItem[]>)) => void;
setAgentStreamHead: (serverId: string, state: Map<string, StreamItem[]> | ((prev: Map<string, StreamItem[]>) => Map<string, StreamItem[]>)) => void; setAgentStreamHead: (serverId: string, state: Map<string, StreamItem[]> | ((prev: Map<string, StreamItem[]>) => Map<string, StreamItem[]>)) => void;
setAgentStreamState: (
serverId: string,
agentId: string,
state: { tail?: StreamItem[]; head?: StreamItem[] }
) => void;
clearAgentStreamHead: (serverId: string, agentId: string) => void; clearAgentStreamHead: (serverId: string, agentId: string) => void;
setAgentTimelineCursor: ( setAgentTimelineCursor: (
serverId: string, serverId: string,
@@ -623,6 +628,67 @@ export const useSessionStore = create<SessionStore>()(
}); });
}, },
setAgentStreamState: (serverId, agentId, state) => {
set((prev) => {
const session = prev.sessions[serverId];
if (!session) {
return prev;
}
let nextTail = session.agentStreamTail;
let nextHead = session.agentStreamHead;
let changedTail = false;
let changedHead = false;
if (state.tail !== undefined) {
const existingTail = session.agentStreamTail.get(agentId);
if (existingTail !== state.tail) {
nextTail = new Map(session.agentStreamTail);
nextTail.set(agentId, state.tail);
changedTail = true;
}
}
if (state.head !== undefined) {
const existingHead = session.agentStreamHead.get(agentId);
const shouldDeleteHead = state.head.length === 0;
if (shouldDeleteHead) {
if (session.agentStreamHead.has(agentId)) {
nextHead = new Map(session.agentStreamHead);
nextHead.delete(agentId);
changedHead = true;
}
} else if (existingHead !== state.head) {
nextHead = new Map(session.agentStreamHead);
nextHead.set(agentId, state.head);
changedHead = true;
}
}
if (!changedTail && !changedHead) {
return prev;
}
logSessionStoreUpdate("setAgentStreamState", serverId, {
agentId,
changedTail,
changedHead,
});
return {
...prev,
sessions: {
...prev.sessions,
[serverId]: {
...session,
agentStreamTail: nextTail,
agentStreamHead: nextHead,
},
},
};
});
},
clearAgentStreamHead: (serverId, agentId) => { clearAgentStreamHead: (serverId, agentId) => {
set((prev) => { set((prev) => {
const session = prev.sessions[serverId]; const session = prev.sessions[serverId];