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
This commit is contained in:
Nicolas Varrot
2026-02-13 10:58:11 +00:00
parent 1c09ccde22
commit ccabb8699f

View File

@@ -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<HTMLAnchorElement>) {
}
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) => (
<div key={`text-${i}`} className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[[rehypeHighlight, rehypeHighlightOptions]]} components={markdownComponents}>
<ReactMarkdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={markdownComponents}>
{autoFormatText((block as Extract<MessageBlock, { type: 'text' }>).text)}
</ReactMarkdown>
</div>
@@ -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) : (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[[rehypeHighlight, rehypeHighlightOptions]]} components={markdownComponents}>
<ReactMarkdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={markdownComponents}>
{autoFormatText(message.content)}
</ReactMarkdown>
</div>
@@ -529,4 +533,4 @@ export function ChatMessageComponent({ message: rawMessage, onRetry, agentAvatar
</div>
</div>
);
}
});