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.
This commit is contained in:
xintaofei
2026-04-14 15:12:14 +08:00
parent f9923df1fe
commit f9c0887346
2 changed files with 48 additions and 46 deletions

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { memo } from "react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import type { import type {
AgentType, AgentType,
@@ -48,7 +49,7 @@ interface ChatInputProps {
onForkSend?: (draft: PromptDraft, modeId?: string | null) => void onForkSend?: (draft: PromptDraft, modeId?: string | null) => void
} }
export function ChatInput({ export const ChatInput = memo(function ChatInput({
status, status,
promptCapabilities, promptCapabilities,
defaultPath, defaultPath,
@@ -138,4 +139,6 @@ export function ChatInput({
/> />
</div> </div>
) )
} })
ChatInput.displayName = "ChatInput"

View File

@@ -1,4 +1,4 @@
import type { ReactNode } from "react" import { useMemo, type ReactNode } from "react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import type { import type {
AgentType, AgentType,
@@ -104,50 +104,48 @@ export function ConversationShell({
onForkSend, onForkSend,
}: ConversationShellProps) { }: ConversationShellProps) {
const tAcp = useTranslations("Folder.chat.acpConnections") const tAcp = useTranslations("Folder.chat.acpConnections")
const retry = claudeApiRetry const retryLineText = useMemo(() => {
const retryAttemptRaw = retry?.attempt const retry = claudeApiRetry
const retryMaxRaw = retry?.maxRetries if (!retry) return null
const retryDelayMsRaw = retry?.retryDelayMs
const retryErrorStatusRaw = retry?.errorStatus
const retryAttempt = const retryAttempt =
retryAttemptRaw !== null && retryAttemptRaw !== undefined retry.attempt !== null && retry.attempt !== undefined
? Math.trunc(retryAttemptRaw) ? Math.trunc(retry.attempt)
: null : null
const retryMax = const retryMax =
retryMaxRaw !== null && retryMaxRaw !== undefined retry.maxRetries !== null && retry.maxRetries !== undefined
? Math.trunc(retryMaxRaw) ? Math.trunc(retry.maxRetries)
: null : null
const retryDelaySeconds = const retryDelaySeconds =
retryDelayMsRaw !== null && retryDelayMsRaw !== undefined retry.retryDelayMs !== null && retry.retryDelayMs !== undefined
? (retryDelayMsRaw / 1000).toFixed(1) ? (retry.retryDelayMs / 1000).toFixed(1)
: null : null
const errorLabel = retry?.error ?? tAcp("claudeApiRetry.fallbackError") const errorLabel = retry.error ?? tAcp("claudeApiRetry.fallbackError")
const statusLabel = const statusLabel =
retryErrorStatusRaw !== null && retryErrorStatusRaw !== undefined retry.errorStatus !== null && retry.errorStatus !== undefined
? tAcp("claudeApiRetry.httpStatus", { ? tAcp("claudeApiRetry.httpStatus", {
status: Math.trunc(retryErrorStatusRaw), status: Math.trunc(retry.errorStatus),
})
: ""
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 = const retryLabel =
retryDelaySeconds !== null retryAttempt !== null && retryMax !== null
? tAcp("claudeApiRetry.nextRetryIn", { ? tAcp("claudeApiRetry.retryingWithMax", {
seconds: retryDelaySeconds, attempt: retryAttempt,
}) max: retryMax,
: null })
const retryLineText = : retryAttempt !== null
delayLabel !== 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", { ? tAcp("claudeApiRetry.lineWithDelay", {
error: errorLabel, error: errorLabel,
status: statusLabel, status: statusLabel,
@@ -159,6 +157,7 @@ export function ConversationShell({
status: statusLabel, status: statusLabel,
retry: retryLabel, retry: retryLabel,
}) })
}, [claudeApiRetry, tAcp])
return ( return (
<div className="flex h-full min-h-0 flex-col"> <div className="flex h-full min-h-0 flex-col">
@@ -207,7 +206,7 @@ export function ConversationShell({
/> />
)} )}
{claudeApiRetry && ( {retryLineText && (
<div className="border-t border-destructive/20 bg-destructive/5 px-4 py-2 text-xs text-destructive"> <div className="border-t border-destructive/20 bg-destructive/5 px-4 py-2 text-xs text-destructive">
<div className="flex items-center gap-2 font-medium"> <div className="flex items-center gap-2 font-medium">
<Loader2 className="h-3.5 w-3.5 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />