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
相关产品推荐
相关产品推荐

