You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 08:55:16