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

如何优化大型动态React状态的维护与渲染性能?

React大规模Cesium点数据渲染性能优化方案

问题背景

我有一个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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 09:27:03