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({
)}
>