如何优化大型动态React状态的维护与渲染性能?
问题背景
我有一个React组件,通过Socket接收数据更新状态,状态中包含约10000个JSON对象,每个对象对应Cesium/Resium应用中要渲染的pointPrimitive。每秒数次编辑该列表的开销极大,即便用了记忆化子组件判断是否重渲染,在这个规模下成本依然很高。已经尝试过扁平化pointsDict、改用immutable.js,也做了减少子组件渲染、控制父组件重渲染的措施,但效果都不理想,希望找到让组件运行速度合理的方案或重构思路。
原组件代码
import React from 'react'; import { useState, useEffect } from 'react'; import { io } from "socket.io-client"; import PointCollection from '../TrackPoints/PointCollection'; import { getServerURL } from '../../lib/getServerURL'; const enoughPoints = 1; const delayMSec = 1000 * .5; const expireSec = 45; //This should depend on the class of item. Maybe anomalous things should last longer const serverURL = getServerURL(); const PointManager = (props: {modes: any, onClick: Function, viewRef}) => { const [pointsDict, setPointsDict] = useState({}); const mode = props.modes.ADSB; //TODO: figure out whether to use a list of JSONs or a proper list of components //There may be performance consequences to either. For now, this is cleaner to code const addPoint = (JSONEntry) => { const k = JSONEntry.react_key; const oldJSON = pointsDict[k] === undefined ? [] : pointsDict[k]; let newJSON = JSONEntry; if (oldJSON.length > 0) { newJSON.isAnomalous = oldJSON[oldJSON.length-1].isAnomalous; } else { newJSON.isAnomalous = false; } setPointsDict((pointsDict) => { const copyFoo = { ...pointsDict}; if(copyFoo[k] === undefined) { copyFoo[k] = [newJSON]; return copyFoo; } copyFoo[k].push(newJSON); globalThis.SCJJ = copyFoo; return copyFoo; }); }; const staleOutPoints = (expireSec: number) => { setPointsDict((pointsDict) => { const keys = Object.keys(pointsDict); const t = Date.now()/1000; let newPoints = {}; keys.map((trackID) => { const pts = pointsDict[trackID]; let ptsFilter = pts.filter((pt,idx) => t - pt.TIMESTAMP < expireSec); newPoints[trackID] = ptsFilter; }); return newPoints; }); }; //Control point time-out useEffect(() => { const interval = setInterval(() => staleOutPoints(expireSec), delayMSec); return () => clearInterval(interval); }, []); //points socket useEffect(() => { const format = (mode == "live" ? "LIVE" : "DEMO"); console.log("Setting up stream with format " + format); const socketIOSocket = io(serverURL,{}); async function fetchStream() { socketIOSocket.on('connect', function() { console.log('connected to points server!') socketIOSocket.emit('please_send_JSON', { "format_requested": format, }) }); socketIOSocket.on('JSON_entry', function(msg) { addPoint(msg); }); } fetchStream(); return () => { console.log("cleanup called"); socketIOSocket.disconnect(); }; }, [mode]); return( <> {/* These are all rendering with every new point. Maybe there's a way to prevent this*/} <PointCollection points={pointsDict}></PointCollection> </> ) }; export default PointManager;
优化方案
1. 减少状态更新范围,避免全量拷贝
当前addPoint每次都会浅拷贝整个pointsDict,哪怕只新增一个点,这会导致整个对象引用变化,触发PointCollection全量重渲染。修改为仅更新单个track对应的数组:
const addPoint = (JSONEntry) => { const k = JSONEntry.react_key; setPointsDict(prev => { const existingTrack = prev[k] || []; const newJSON = { ...JSONEntry, isAnomalous: existingTrack.length > 0 ? existingTrack[existingTrack.length - 1].isAnomalous : false }; // 仅更新当前track的数组,不拷贝整个字典 return { ...prev, [k]: [...existingTrack, newJSON] }; }); };
这样每次更新只会改变单个key的引用,配合子组件的记忆化,能大幅减少不必要的重渲染。
2. 优化过期清理逻辑,避免无意义状态更新
当前staleOutPoints每次遍历所有track并创建新对象,哪怕大部分track没有过期点。修改为仅清理有过期点的track,且只有当数组长度变化时才更新引用:
const staleOutPoints = (expireSec) => { setPointsDict(prev => { const t = Date.now() / 1000; const updated = {}; let hasChanges = false; Object.entries(prev).forEach(([trackID, pts]) => { const filtered = pts.filter(pt => t - pt.TIMESTAMP < expireSec); if (filtered.length !== pts.length) { updated[trackID] = filtered; hasChanges = true; } else { // 无变化则保留原引用,避免触发重渲染 updated[trackID] = pts; } }); // 仅当有实际变化时才返回新对象,否则复用原状态 return hasChanges ? updated : prev; }); };
如果某次清理没有任何track的点数量变化,就不会触发状态更新,避免无用的重渲染。
3. 精细化子组件渲染,仅更新变化的track
改造PointCollection,拆分每个track对应的点列表,用React.memo包裹单个track的组件,并给每个子组件稳定的key:
// PointCollection.jsx import React from 'react'; import TrackPoints from './TrackPoints'; // 用React.memo包裹,仅当points引用变化时重渲染 const PointCollection = React.memo(({ points }) => { return ( <> {Object.entries(points).map(([trackID, pts]) => ( <TrackPoints key={trackID} points={pts} trackID={trackID} /> ))} </> ); }); export default PointCollection;
然后TrackPoints组件同样用React.memo包裹,确保只有当pts引用变化时才重渲染——结合前两步的优化,只有对应的track数组变化时,pts的引用才会改变,其他track的组件不会被触发重渲染。
4. 绕过React状态,直接操作Cesium原生API
如果React状态管理的开销无法接受,可以直接在Socket回调中操作Cesium的Primitive/Entity集合,完全绕开React的渲染流程:
const PointManager = (props: {modes: any, onClick: Function, viewRef}) => { // 用ref存储原生Cesium实体,不触发React重渲染 const trackEntities = React.useRef(new Map()); useEffect(() => { const socketIOSocket = io(serverURL,{}); const viewer = props.viewRef.current?.viewer; if (!viewer) return; socketIOSocket.on('JSON_entry', (msg) => { const k = msg.react_key; let entity = trackEntities.current.get(k); if (!entity) { // 创建新的Cesium Entity entity = viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(msg.lon, msg.lat, msg.alt), point: { pixelSize: 5, color: Cesium.Color.RED }, // 存储时间戳用于过期判断 lastUpdatedTime: msg.TIMESTAMP }); trackEntities.current.set(k, entity); } else { // 更新现有实体的位置和时间戳 entity.position = Cesium.Cartesian3.fromDegrees(msg.lon, msg.lat, msg.alt); entity.lastUpdatedTime = msg.TIMESTAMP; // 继承之前的异常状态 entity.point.color = entity.isAnomalous ? Cesium.Color.YELLOW : Cesium.Color.RED; } // 同步异常状态 if (trackEntities.current.has(k)) { const prevEntity = trackEntities.current.get(k); entity.isAnomalous = prevEntity.isAnomalous ?? false; } }); // 过期清理定时器 const interval = setInterval(() => { const t = Date.now() / 1000; trackEntities.current.forEach((entity, k) => { if (t - entity.lastUpdatedTime > expireSec) { viewer.entities.remove(entity); trackEntities.current.delete(k); } }); }, delayMSec); return () => { socketIOSocket.disconnect(); clearInterval(interval); // 清理所有实体 trackEntities.current.forEach(entity => viewer.entities.remove(entity)); trackEntities.current.clear(); }; }, [mode]); return null; // 无需渲染React组件,直接操作Cesium };
这种方式性能提升最显著,但代价是失去React的声明式管理,需要手动处理生命周期和数据同步,适合性能要求极高的场景。
5. 批量处理Socket消息,降低状态更新频率
如果Socket每秒推送多次消息,可将消息缓存后批量处理,减少setPointsDict的调用次数:
const PointManager = (props) => { const messageQueue = React.useRef([]); const queueTimer = React.useRef(null); const processQueue = () => { if (messageQueue.current.length === 0) return; setPointsDict(prev => { const newDict = { ...prev }; messageQueue.current.forEach(msg => { const k = msg.react_key; const existingTrack = newDict[k] || []; const newJSON = { ...msg, isAnomalous: existingTrack.length > 0 ? existingTrack[existingTrack.length - 1].isAnomalous : false }; newDict[k] = [...existingTrack, newJSON]; }); messageQueue.current = []; return newDict; }); }; useEffect(() => { const socketIOSocket = io(serverURL,{}); socketIOSocket.on('JSON_entry', (msg) => { messageQueue.current.push(msg); if (!queueTimer.current) { // 每100ms批量处理一次消息,可根据需求调整间隔 queueTimer.current = setTimeout(processQueue, 100); } }); return () => { socketIOSocket.disconnect(); clearTimeout(queueTimer.current); }; }, [mode]); // ... 其他逻辑 };
将多次状态更新合并为一次,减少React的渲染次数。
内容的提问来源于stack exchange,提问作者ResiumRegent

