From f9c088734656f550ead2d7bd55c249768ce0ba6b Mon Sep 17 00:00:00 2001 From: xintaofei Date: Tue, 14 Apr 2026 15:12:14 +0800 Subject: [PATCH] perf(chat): reduce input rerender overhead in conversation shell Memoize ChatInput to minimize unnecessary updates in the chat input area during frequent parent renders. Compute localized Claude API retry banner text lazily and only when retry state is present. --- src/components/chat/chat-input.tsx | 7 +- src/components/chat/conversation-shell.tsx | 87 +++++++++++----------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/src/components/chat/chat-input.tsx b/src/components/chat/chat-input.tsx index 9fac5bb..f5be27d 100644 --- a/src/components/chat/chat-input.tsx +++ b/src/components/chat/chat-input.tsx @@ -1,5 +1,6 @@ "use client" +import { memo } from "react" import { useTranslations } from "next-intl" import type { AgentType, @@ -48,7 +49,7 @@ interface ChatInputProps { onForkSend?: (draft: PromptDraft, modeId?: string | null) => void } -export function ChatInput({ +export const ChatInput = memo(function ChatInput({ status, promptCapabilities, defaultPath, @@ -138,4 +139,6 @@ export function ChatInput({ /> ) -} +}) + +ChatInput.displayName = "ChatInput" diff --git a/src/components/chat/conversation-shell.tsx b/src/components/chat/conversation-shell.tsx index 50e0a62..78ba43a 100644 --- a/src/components/chat/conversation-shell.tsx +++ b/src/components/chat/conversation-shell.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react" +import { useMemo, type ReactNode } from "react" import { useTranslations } from "next-intl" import type { AgentType, @@ -104,50 +104,48 @@ export function ConversationShell({ onForkSend, }: ConversationShellProps) { const tAcp = useTranslations("Folder.chat.acpConnections") - const retry = claudeApiRetry - const retryAttemptRaw = retry?.attempt - const retryMaxRaw = retry?.maxRetries - const retryDelayMsRaw = retry?.retryDelayMs - const retryErrorStatusRaw = retry?.errorStatus + const retryLineText = useMemo(() => { + const retry = claudeApiRetry + if (!retry) return null - const retryAttempt = - retryAttemptRaw !== null && retryAttemptRaw !== undefined - ? Math.trunc(retryAttemptRaw) - : null - const retryMax = - retryMaxRaw !== null && retryMaxRaw !== undefined - ? Math.trunc(retryMaxRaw) - : null - const retryDelaySeconds = - retryDelayMsRaw !== null && retryDelayMsRaw !== undefined - ? (retryDelayMsRaw / 1000).toFixed(1) - : null - const errorLabel = retry?.error ?? tAcp("claudeApiRetry.fallbackError") - const statusLabel = - retryErrorStatusRaw !== null && retryErrorStatusRaw !== undefined - ? tAcp("claudeApiRetry.httpStatus", { - status: Math.trunc(retryErrorStatusRaw), - }) - : "" - const retryLabel = - retryAttempt !== null && retryMax !== null - ? tAcp("claudeApiRetry.retryingWithMax", { - attempt: retryAttempt, - max: retryMax, - }) - : retryAttempt !== null - ? tAcp("claudeApiRetry.retryingAttempt", { - attempt: retryAttempt, + const retryAttempt = + retry.attempt !== null && retry.attempt !== undefined + ? Math.trunc(retry.attempt) + : null + const retryMax = + retry.maxRetries !== null && retry.maxRetries !== undefined + ? Math.trunc(retry.maxRetries) + : null + const retryDelaySeconds = + retry.retryDelayMs !== null && retry.retryDelayMs !== undefined + ? (retry.retryDelayMs / 1000).toFixed(1) + : null + const errorLabel = retry.error ?? tAcp("claudeApiRetry.fallbackError") + const statusLabel = + retry.errorStatus !== null && retry.errorStatus !== undefined + ? tAcp("claudeApiRetry.httpStatus", { + status: Math.trunc(retry.errorStatus), }) - : tAcp("claudeApiRetry.retrying") - const delayLabel = - retryDelaySeconds !== null - ? tAcp("claudeApiRetry.nextRetryIn", { - seconds: retryDelaySeconds, - }) - : null - const retryLineText = - delayLabel !== null + : "" + const retryLabel = + retryAttempt !== null && retryMax !== null + ? tAcp("claudeApiRetry.retryingWithMax", { + attempt: retryAttempt, + max: retryMax, + }) + : retryAttempt !== null + ? tAcp("claudeApiRetry.retryingAttempt", { + attempt: retryAttempt, + }) + : tAcp("claudeApiRetry.retrying") + const delayLabel = + retryDelaySeconds !== null + ? tAcp("claudeApiRetry.nextRetryIn", { + seconds: retryDelaySeconds, + }) + : null + + return delayLabel !== null ? tAcp("claudeApiRetry.lineWithDelay", { error: errorLabel, status: statusLabel, @@ -159,6 +157,7 @@ export function ConversationShell({ status: statusLabel, retry: retryLabel, }) + }, [claudeApiRetry, tAcp]) return (
@@ -207,7 +206,7 @@ export function ConversationShell({ /> )} - {claudeApiRetry && ( + {retryLineText && (