From ccabb8699f4816a1619a78f7b0452db09a2327c0 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Fri, 13 Feb 2026 10:58:11 +0000 Subject: [PATCH] perf: memoize ChatMessage component and hoist ReactMarkdown plugin arrays - Wrap ChatMessageComponent with React.memo to skip re-renders when props unchanged - Hoist remarkPlugins and rehypePlugins arrays to module scope to avoid creating new array references on every render (prevents unnecessary ReactMarkdown re-processing) - Use proper PluggableList type from unified instead of any --- src/components/ChatMessage.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index be1ec0c..aa8c066 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -1,5 +1,5 @@ -import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; +import { useState, useCallback, useRef, useEffect, useMemo, memo } from 'react'; import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -167,11 +167,15 @@ function MarkdownLink(props: React.AnchorHTMLAttributes) { } const markdownComponents = { pre: CodeBlock, img: MarkdownImage, a: MarkdownLink }; +// Hoisted to module scope to avoid creating new array references each render +import type { PluggableList } from 'unified'; +const remarkPlugins: PluggableList = [remarkGfm, remarkBreaks]; +const rehypePlugins: PluggableList = [[rehypeHighlight, rehypeHighlightOptions]]; function renderTextBlocks(blocks: MessageBlock[]) { return getTextBlocks(blocks).map((block, i) => (
- + {autoFormatText((block as Extract).text)}
@@ -381,7 +385,7 @@ function SystemEventMessage({ message }: { message: ChatMessageType }) { ); } -export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatarUrl }: { message: ChatMessageType; onRetry?: (text: string) => void; agentAvatarUrl?: string }) { +export const ChatMessageComponent = memo(function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatarUrl }: { message: ChatMessageType; onRetry?: (text: string) => void; agentAvatarUrl?: string }) { useLocale(); // re-render on locale change const { resolvedTheme } = useTheme(); const isLight = resolvedTheme === 'light'; @@ -476,7 +480,7 @@ export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatar {/* User-visible text */} {message.blocks.length > 0 ? renderTextBlocks(message.blocks) : (
- + {autoFormatText(message.content)}
@@ -529,4 +533,4 @@ export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatar ); -} +});