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

React JS多元素批量重渲染优化方案咨询

大数量单词渲染性能优化与拖拽重渲染问题

我将文本文件中的单词包装成span在网页展示,单词量少的时候运行正常,但数量达到1000及以上时,网站因重渲染问题变慢。拖拽选择某个span时,所有1000多个span都会被重渲染。试过react.window但没法实现类似文本文件的排版效果,只能显示为单行或网格,不符合需求。希望在保持文本排版的同时提升性能,也请指出我代码中的问题。我是React新手,这是第一个项目。

现有代码

单词包装为span的渲染函数

function wrapTextInSpans() {
  return spanData.map((span, index) => {
    if (!span.selected) {
      return (
        <Box
          key={index}
          id={"textSpan" + span.index}
          className="textSpans"
          onMouseDown={handleMouseDown}
          onMouseMove={handleMouseMove}
          onMouseUp={handleMouseUp}
          bg={span.color}
        >
          {span.text}
        </Box>
      );
    }

    const isFirst = span.isFirst;
    const isLast = span.isLast;
    const isColored = span.isColored;
    const showTagAndCancel = isLast && isColored;

    return (
      <Box
        key={index}
        id={"textSpan" + span.index}
        className={`textSpans selected ${isFirst ? 'first-span' : ''} ${isLast ? 'last-span' : ''}`}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
        bg={span.color}
      >
        {span.text}
        {showTagAndCancel && (
          <Flex padding={2}>
            <Flex
              justifyContent="center"
              alignItems="center"
              bg="gray"
              padding={1}
              margin="0px 4px"
              height="20px"
              borderRadius="2px"
            >
              {span.tag}
            </Flex>
            <Button
              onClick={() => handleSpanCancel(span.index)}
              margin="2px 5px"
              borderRadius="50%"
              sx={{
                minWidth: "20px",
                maxWidth: "20px",
              }}
              height="20px"
              padding={0}
              border="none"
              _hover={{ cursor: "pointer", transform: "scale(0.8)" }}
              zIndex={0}
            >
              <span style={{ padding: "1px", transform: "scale(0.8)", margin: "0", color: "gray" }}>X</span>
            </Button>
          </Flex>
        )}
      </Box>
    );
  });
}

spanData状态生成逻辑

useEffect(() => {
  if (text == "") {
    setStartSpanIndex(null);
    setEndSpanIndex(null);
    setIsMouseDown(false);
    setIsDropdownVisible(false);
    setDropdownPosition({ left: 0, top: 0, width: 0 });
    setIsMouseDragged(false);
    setSpanData([]);
    setStartSpanPosition({ left: null, top: null });
    setEndSpanPosition({ left: null, top: null });
    setIsSpanIndexUpdated(false);
  } else {
    let validIndex = 0;
    const words = text.split(" ");
    let startOffset = 0
    let endOffset = -1
    const tempSpanData = words.map((word) => {
      if (word.trim() === "") {
        return null; // Skip creating span
      } else {
        validIndex++;
        startOffset = endOffset + 1;
        endOffset = startOffset + word.length;
        return {
          id: "textSpan" + validIndex,
          selected: false,
          index: validIndex,
          color: "",
          isFirst: false,
          isLast: false,
          isColored: false,
          text: word,
          startOffSet: startOffset,
          endOffSet: endOffset,
          tag: null,
          spanRange: null,
        };
      }
    });
    setSpanData(tempSpanData.filter(span => span !== null)); // Remove null elements
  }
}, [text]);

代码中的核心问题

  • key值错误:用数组index作为组件key,当spanData顺序变化或元素更新时,React会错误复用组件,导致不必要的重渲染,拖拽选择时所有组件都会被检查。
  • 回调函数未缓存:handleMouseDown等事件回调每次组件渲染都会生成新的函数实例,导致每个Box组件因为props变化触发重渲染。
  • 全量状态更新:拖拽选择时修改整个spanData数组,触发父组件全量重渲染,进而所有子Box组件都跟着重渲染。
  • 组件粒度太细:每个单词都作为独立Box组件,1000个组件的渲染成本本身就很高,且没有任何缓存机制。

优化方案

1. 修复key值

把key从数组index改成每个span独有的span.id或span.index,确保key唯一且稳定:

<Box
  key={span.id} // 改用span的唯一标识
  // ...其他props
>

2. 用useCallback缓存事件回调

用useCallback包裹鼠标事件回调,避免每次渲染生成新函数:

import { useCallback } from 'react';

const handleMouseDown = useCallback((e) => {
  // 原有的鼠标按下逻辑
}, [/* 这里填用到的状态依赖,比如setIsMouseDown、startSpanIndex等 */]);

const handleMouseMove = useCallback((e) => {
  // 原有的鼠标移动逻辑
}, [/* 依赖项 */]);

const handleMouseUp = useCallback((e) => {
  // 原有的鼠标抬起逻辑
}, [/* 依赖项 */]);

3. 拆分组件并使用React.memo缓存

把单个单词的Box抽成独立组件,用React.memo包裹,只有当props真正变化时才重渲染:

import { memo } from 'react';

const WordSpan = memo(({ span, handleMouseDown, handleMouseMove, handleMouseUp, handleSpanCancel }) => {
  if (!span.selected) {
    return (
      <Box
        id={span.id}
        className="textSpans"
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
        bg={span.color}
      >
        {span.text}
      </Box>
    );
  }

  const isFirst = span.isFirst;
  const isLast = span.isLast;
  const isColored = span.isColored;
  const showTagAndCancel = isLast && isColored;

  return (
    <Box
      id={span.id}
      className={`textSpans selected ${isFirst ? 'first-span' : ''} ${isLast ? 'last-span' : ''}`}
      onMouseDown={handleMouseDown}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
      bg={span.color}
    >
      {span.text}
      {showTagAndCancel && (
        <Flex padding={2}>
          <Flex
            justifyContent="center"
            alignItems="center"
            bg="gray"
            padding={1}
            margin="0px 4px"
            height="20px"
            borderRadius="2px"
          >
            {span.tag}
          </Flex>
          <Button
            onClick={() => handleSpanCancel(span.index)}
            margin="2px 5px"
            borderRadius="50%"
            sx={{
              minWidth: "20px",
              maxWidth: "20px",
            }}
            height="20px"
            padding={0}
            border="none"
            _hover={{ cursor: "pointer", transform: "scale(0.8)" }}
            zIndex={0}
          >
            <span style={{ padding: "1px", transform: "scale(0.8)", margin: "0", color: "gray" }}>X</span>
          </Button>
        </Flex>
      )}
    </Box>
  );
});

// 原渲染函数修改为
function wrapTextInSpans() {
  return spanData.map((span) => (
    <WordSpan
      key={span.id}
      span={span}
      handleMouseDown={handleMouseDown}
      handleMouseMove={handleMouseMove}
      handleMouseUp={handleMouseUp}
      handleSpanCancel={handleSpanCancel}
    />
  ));
}

4. 优化状态更新逻辑

用immer库实现不可变的局部状态更新,只修改需要变化的span项,减少状态变化范围:

import { produce } from 'immer';

// 比如更新选中状态时
setSpanData(prev => produce(prev, draft => {
  // 仅修改选中范围内的span
  for (let i = startIndex; i <= endIndex; i++) {
    if (draft[i]) {
      draft[i].selected = true;
      draft[i].isFirst = i === startIndex;
      draft[i].isLast = i === endIndex;
    }
  }
}));

这样只有被修改的span会触发重渲染,配合memo的子组件,性能会大幅提升。

5. 实现支持文本排版的虚拟滚动

如果react.window不符合需求,可以用@tanstack/react-virtual实现自定义虚拟滚动:

  • 先将文本按行拆分,测量每行的高度;
  • 用虚拟滚动只渲染可视区域内的行,每行内渲染对应的单词span;
  • 这样既保持文本的自然排版,又减少DOM节点数量,解决大数量单词的渲染性能问题。

内容的提问来源于stack exchange,提问作者Dilip

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 02:07:04