From 628ce5bb866582f79fe9cc3ef2866450f0533396 Mon Sep 17 00:00:00 2001 From: b Date: Mon, 27 Jul 2026 01:48:26 -0500 Subject: [PATCH] perf(desktop): bail the transcript out of router-driven re-renders on session switch --- .../components/assistant-ui/thread/index.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/thread/index.tsx b/apps/desktop/src/components/assistant-ui/thread/index.tsx index 2f26b75a4ed5..8be74ba9e107 100644 --- a/apps/desktop/src/components/assistant-ui/thread/index.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/index.tsx @@ -1,4 +1,4 @@ -import { type FC, useCallback, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useMemo, useRef, useState } from 'react' import { AssistantMessage } from '@/components/assistant-ui/thread/assistant-message' import { ThreadMessageList } from '@/components/assistant-ui/thread/list' @@ -16,7 +16,7 @@ import { notifyError } from '@/store/notifications' type ThreadLoadingState = 'response' | 'session' -export const Thread: FC<{ +interface ThreadProps { clampToComposer?: boolean cwd?: string | null gateway?: HermesGateway | null @@ -28,7 +28,16 @@ export const Thread: FC<{ onRestoreToMessage?: (messageId: string, target?: RestoreMessageTarget) => Promise | void sessionId?: string | null sessionKey?: string | null -}> = ({ +} + +// memo'd on purpose, and load-bearing for session-switch cost. ChatView +// re-renders on every route change (it reads `location`), and this subtree is +// the entire transcript — without a bail-out here the router's context update +// rebuilds every message of the OUTGOING thread before it is replaced. The +// props above are all stable across a plain re-render (see the component-map +// and loadingIndicator memos below), so the only thing that gets through is a +// genuine change. +export const Thread = memo(function Thread({ clampToComposer = false, cwd = null, gateway = null, @@ -40,7 +49,7 @@ export const Thread: FC<{ onRestoreToMessage, sessionId = null, sessionKey -}) => { +}: ThreadProps) { const { t } = useI18n() const copy = t.assistant.thread @@ -129,13 +138,20 @@ export const Thread: FC<{ ) : undefined + // Stable element identity, for the same reason the component map above is + // memoized: this is a prop of the memo'd ThreadMessageList, so a fresh + // element every render defeats the bail-out and drags the whole transcript + // into the switch's render pass. It takes no props, so one element is + // always correct. + const loadingIndicator = useMemo(() => , []) + return (
} + loadingIndicator={loadingIndicator} sessionKey={sessionKey} /> {loading === 'session' && } @@ -151,4 +167,4 @@ export const Thread: FC<{ />
) -} +})