React Markdown+MathJax打字效果引发性能问题优化咨询
优化React Markdown打字效果的重渲染性能问题
问题背景
使用React Markdown渲染带打字效果的文本时,每添加一个字符就会触发整个文本的重渲染,导致明显的性能问题。相关代码如下:
const MathJaxRenderer: FC<{ text: string }> = memo(({ text }) => { return ( <MathJax> <ReactMarkdown remarkPlugins={[remarkGfm]} components={{ p: ({children}) => ( <p style={{ margin: '1em 0' }}> {children} </p> ), code: ({children, inline}) => { if (inline) { return <span>{children}</span>; } return ( <div style={{ margin: '1em 0' }}> {children} </div> ); } }} > {text} </ReactMarkdown> </MathJax> ); }); const typeNextChar = () => { if (charIndex < fullText.length && fullText[charIndex + 1]) { setChatMessages(prevState => { const updatedMessages = [...prevState]; const currentMessage = updatedMessages[index]; if (currentMessage) { const currentDisplayText = currentMessage.displayText || ''; currentMessage.displayText = currentDisplayText + fullText[charIndex]; charIndex++; } return updatedMessages; }); requestAnimationFrame(typeNextChar); } else { setChatMessages(prevState => { const updatedMessages = [...prevState]; if (updatedMessages[index]) { updatedMessages[index].isTyping = false; } return updatedMessages; }); } }; typeNextChar();
优化方案
1. 拆分静态与动态渲染,避免全量重渲染
核心思路是将已完成渲染的Markdown内容与正在输入的纯文本分离:
- 维护两个状态:
completedText(已完成打字的文本,仅在打字暂停/结束时更新)和typingText(当前正在输入的字符片段) completedText通过MathJaxRenderer渲染为Markdown格式,typingText直接以纯文本形式显示- 打字完成后,将
completedText与typingText合并,统一进行Markdown渲染
示例代码:
// 组件内状态 const [completedText, setCompletedText] = useState(''); const [typingText, setTypingText] = useState(''); const [isTyping, setIsTyping] = useState(true); const [charIndex, setCharIndex] = useState(0); const typeNextChar = () => { if (charIndex < fullText.length) { const nextChar = fullText[charIndex]; setTypingText(prev => prev + nextChar); setCharIndex(prev => prev + 1); requestAnimationFrame(typeNextChar); } else { setCompletedText(prev => prev + typingText); setTypingText(''); setIsTyping(false); } }; // 渲染部分 return ( <div> <MathJaxRenderer text={completedText} /> {isTyping && <span>{typingText}</span>} </div> );
2. 优化状态更新逻辑,减少不必要的数组复制
原代码中每次更新消息时都会复制整个chatMessages数组,且直接修改对象属性,容易触发无关组件的重渲染:
- 使用
useState管理charIndex,避免直接修改变量导致状态不一致 - 更新消息时仅修改目标消息的属性,保持其他消息的引用不变
示例代码:
// 用useState管理charIndex const [charIndex, setCharIndex] = useState(0); const typeNextChar = () => { if (charIndex < fullText.length) { setChatMessages(prev => prev.map((msg, i) => { if (i !== index) return msg; // 非目标消息直接返回原引用 return { ...msg, displayText: (msg.displayText || '') + fullText[charIndex] }; })); setCharIndex(prev => prev + 1); requestAnimationFrame(typeNextChar); } else { setChatMessages(prev => prev.map((msg, i) => { if (i !== index) return msg; return {...msg, isTyping: false}; })); } };
3. 延迟Markdown解析至打字完成
打字过程中用户通常不需要立即看到Markdown格式(如加粗、代码块),可以暂时用纯文本显示,待打字结束后再触发Markdown解析:
- 维护
isTyping状态,为true时直接渲染纯文本,为false时再使用MathJaxRenderer
示例代码:
// 渲染当前消息 const currentMsg = chatMessages[index]; return ( <div> {currentMsg.isTyping ? ( <span>{currentMsg.displayText}</span> ) : ( <MathJaxRenderer text={currentMsg.displayText} /> )} </div> );
4. 强化MathJaxRenderer的缓存机制
确保memo能正确生效,避免text变化时不必要的重渲染:
- 将自定义组件提取到外部,保持引用稳定
- 使用
useMemo缓存ReactMarkdown的渲染结果,仅在text变化时更新
示例代码:
// 将自定义组件提取到外部,保持引用稳定 const CustomP = ({children}: {children: React.ReactNode}) => ( <p style={{ margin: '1em 0' }}>{children}</p> ); const CustomCode = ({children, inline}: {children: React.ReactNode, inline?: boolean}) => { if (inline) return <span>{children}</span>; return <div style={{ margin: '1em 0' }}>{children}</div>; }; const MathJaxRenderer: FC<{ text: string }> = memo(({ text }) => { const renderedContent = useMemo(() => ( <ReactMarkdown remarkPlugins={[remarkGfm]} components={{p: CustomP, code: CustomCode}} > {text} </ReactMarkdown> ), [text]); return <MathJax>{renderedContent}</MathJax>; });
内容的提问来源于stack exchange,提问作者Nadav Holtzman
相关产品推荐
相关产品推荐

