perf: reduce chat streaming rerenders

- Keep historical chat messages stable during streaming
- Isolate active stream state to the current message path
- Reduce chat container churn from sync message updates
This commit is contained in:
Bohdan Triapitsyn
2026-04-05 16:56:05 +03:00
parent e56f2f99cf
commit c88e68e5f7
4 changed files with 93 additions and 30 deletions
@@ -8,7 +8,6 @@ import { useConfigStore } from '@/stores/useConfigStore';
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useUIStore } from '@/stores/useUIStore';
import { useContextStore } from '@/stores/contextStore';
import { useStreamingStore } from '@/sync/streaming';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSelectionStore } from '@/sync/selection-store';
import * as sessionActions from '@/sync/session-actions';
@@ -131,6 +130,7 @@ interface ChatMessageProps {
turnGroupingContext?: TurnGroupingContext;
assistantHeaderMessageId?: string;
isInActiveTurn?: boolean;
activeStreamingPhase?: StreamPhase | null;
animateUserOnMount?: boolean;
onUserAnimationConsumed?: (messageId: string) => void;
}
@@ -144,6 +144,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
turnGroupingContext,
assistantHeaderMessageId,
isInActiveTurn = false,
activeStreamingPhase = null,
animateUserOnMount = false,
onUserAnimationConsumed,
}) => {
@@ -152,13 +153,6 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
const messageContainerRef = React.useRef<HTMLDivElement | null>(null);
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const streamState = useStreamingStore((s) => s.messageStreamStates.get(message.info.id));
const lifecyclePhase = isInActiveTurn ? (streamState?.phase ?? null) : null;
const msgSessionId = (message.info as { sessionID?: string }).sessionID ?? currentSessionId ?? null;
const streamingMsgForSession = useStreamingStore((s) => msgSessionId ? s.streamingMessageIds.get(msgSessionId) ?? null : null);
const isStreamingMessage = isInActiveTurn ? streamingMsgForSession === message.info.id : false;
const hasActiveStreamInSession = typeof streamingMsgForSession === 'string' && streamingMsgForSession.length > 0;
const getAgentModelForSession = useSelectionStore((s) => s.getAgentModelForSession);
const getSessionModelSelection = useSelectionStore((s) => s.getSessionModelSelection);
@@ -166,13 +160,8 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
const forkFromMessage = sessionActions.forkFromMessage;
streamPerfCount('ui.chat_message.render');
if (isStreamingMessage) {
if (isInActiveTurn) {
streamPerfCount('ui.chat_message.render.streaming');
} else if (hasActiveStreamInSession) {
streamPerfCount('ui.chat_message.render.static_during_stream');
if (!isInActiveTurn) {
streamPerfCount('ui.chat_message.render.static_outside_active_turn_during_stream');
}
}
const providers = useConfigStore.getState().providers;
@@ -596,11 +585,11 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
if (isMessageCompleted) {
return 'completed';
}
if (lifecyclePhase) {
return lifecyclePhase;
if (isInActiveTurn) {
return activeStreamingPhase ?? 'streaming';
}
return isStreamingMessage ? 'streaming' : 'completed';
}, [isMessageCompleted, lifecyclePhase, isStreamingMessage]);
return 'completed';
}, [activeStreamingPhase, isInActiveTurn, isMessageCompleted]);
React.useEffect(() => {
if (!isUser || !animateUserOnMount) {
@@ -1148,6 +1137,7 @@ export default React.memo(ChatMessage, (prev, next) => {
&& prev.turnGroupingContext === next.turnGroupingContext
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId
&& prev.isInActiveTurn === next.isInActiveTurn
&& prev.activeStreamingPhase === next.activeStreamingPhase
&& prev.animateUserOnMount === next.animateUserOnMount
&& prev.onUserAnimationConsumed === next.onUserAnimationConsumed;
});