From 56454af93163ca7c373656d4865fe3e2c783a9ae Mon Sep 17 00:00:00 2001 From: hataa <79907651+hata33@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:28:43 +0800 Subject: [PATCH] perf(frontend): cache settled copy-data derivation across streaming chunks (#5095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(frontend): cache settled copy-data derivation across streaming chunks Every SSE values chunk re-renders MessageList, and the re-render re-derived copy/toolbar text for every settled row: getAssistantTurnCopyData re-ran the O(turn bytes) content extraction per settled group, and MessageListItem's toolbar recomputed getMessageCopyData per message. Settled group arrays keep their identity across chunks (deriveStableMessageGroups), so both derivations now cache on that stable reference: a WeakMap keyed on the messages array for turn copy data, and a useMemo on message identity for the toolbar copy text. Fixes #5094 * fix(frontend): gate row copy-data memo and correct cache win claim Address review: derive one memoized copy value only when isHuman || (!isLoading && showCopyButton) and reuse it for both editing and the toolbar, so settled assistant rows (whose toolbar never renders) skip the derivation and human rows derive once, not twice; correct the assistantTurnCopyDataCache comment — the regex/trim split is already cached per message, the cache's win is the traversal/allocations for string turns and the uncached O(bytes) map/join/trim for array-content turns (benchmarked: 5.1x / 17.2x per settled history sweep). * style(frontend): expand single-line messages array for Prettier --- .../workspace/messages/message-list-item.tsx | 17 ++- frontend/src/core/messages/utils.ts | 26 +++- .../messages/message-list-item.dom.test.tsx | 118 ++++++++++++++++++ .../tests/unit/core/messages/utils.test.ts | 51 ++++++++ 4 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 frontend/tests/unit/components/workspace/messages/message-list-item.dom.test.tsx diff --git a/frontend/src/components/workspace/messages/message-list-item.tsx b/frontend/src/components/workspace/messages/message-list-item.tsx index 1a0c2f008..05bb4c0f5 100644 --- a/frontend/src/components/workspace/messages/message-list-item.tsx +++ b/frontend/src/components/workspace/messages/message-list-item.tsx @@ -165,10 +165,19 @@ export function MessageListItem({ }) { const { t } = useI18n(); const isHuman = message.type === "human"; - const editableText = useMemo( - () => (isHuman ? (getMessageCopyData(message) ?? "") : ""), - [isHuman, message], + // One derivation serves both editing and the toolbar, and only runs when + // either consumer can use it: assistant rows never render this toolbar + // (the sole call site passes showCopyButton only for non-assistant rows) + // and the toolbar stays unrendered while loading — matching the guard the + // pre-memo call sat behind instead of deriving for every settled row. + const copyData = useMemo( + () => + isHuman || (!isLoading && showCopyButton) + ? (getMessageCopyData(message) ?? "") + : "", + [isHuman, isLoading, showCopyButton, message], ); + const editableText = isHuman ? copyData : ""; const [isEditing, setIsEditing] = useState(false); const [draft, setDraft] = useState(""); const [isSubmittingEdit, setIsSubmittingEdit] = useState(false); @@ -239,7 +248,7 @@ export function MessageListItem({ )} >
- + {canEdit && isHuman && onEditAndRegenerate && !isEditing && (