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

React集成Three.js问题:组件重渲染与Suspense报错排查

问题描述

我正在尝试将原生Three.js项目迁移到React,当前遇到两个问题:

  1. 每次生成新的点时,Sampler组件都会重新渲染,该如何优化?
  2. 使用Suspense时出现报错:Cannot update a component (\Loader`) while rendering a different component (`Sampler`)`,报错原因是什么?

相关代码如下:

const pos = [];
const cols = [];

const Sampler = ({ dotCount, setDotCount }) => {
  /**
   * animations
   */
  useFrame(({ clock }) => {});

  /**
   * options
   */
  const palette = [
    new THREE.Color("#FAAD80"),
    new THREE.Color("#FF6767"),
    new THREE.Color("#FF3D68"),
    new THREE.Color("#A73489"),
  ];

  const tempPosition = new Vector3();

  const colorMap = useLoader(
    TextureLoader,
    "https://assets.codepen.io/127738/dotTexture.png"
  );

  /**
   * whale
   */
  const whaleMaterials = new THREE.MeshBasicMaterial({
    wireframe: true,
    color: 0x000000,
    transparent: true,
    opacity: 0.05,
  });
  const obj = useLoader(OBJLoader, "/textures/whale/Mesh_Whale.obj");
  obj.children[0].material = whaleMaterials;

  const samplerWhale = new MeshSurfaceSampler(obj.children[0]);
  samplerWhale.build();

  //   const cols = [];

  const [positions, colors] = useMemo(() => {
    samplerWhale.sample(tempPosition);
    pos.push(tempPosition.x, tempPosition.y, tempPosition.z);
    const randomColor = palette[Math.floor(Math.random() * palette.length)];
    cols.push(randomColor.r, randomColor.g, randomColor.b, 1);

    return [new Float32Array(pos), new Float32Array(cols)];
  }, [dotCount]);

  useEffect(() => {
    if (obj === "undefined") return;

    const t = setInterval(() => {
      if (dotCount < 100) {
        setDotCount((prev) => prev + 1);
      }
    }, 100);

    return () => clearTimeout(t);
  }, []);

  return (
    <group>
      <primitive
        object={obj}
        wireframe={true}
        color={0x000000}
        transparent={true}
        opacity={0.05}
      />
      <points>
        <bufferGeometry attach="geometry">
          <bufferAttribute
            attach={"attributes-position"}
            args={[positions, 3]}
          />
          <bufferAttribute attach={"attributes-color"} args={[colors, 4]} />
        </bufferGeometry>
        <pointsMaterial
          attach="material"
          size={1}
          alphaTest={0.2}
          map={colorMap}
          vertexColors={true}
        />
      </points>
    </group>
  );
};
const SurfaceSampling = () => {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  const [dotCount, setDotCount] = useState(20);

  useEffect(() => {
    setWindowSize({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  }, []);

  return (
    <StyledTHREEContainer windowSize={windowSize}>
      <Canvas
        camera={{
          fov: 75,
          near: 0.1,
          far: 1000,
          aspect: window.innerWidth / window.innerHeight,
          position: [0, 100, 230],
        }}
        gl={{ antialias: false }}
      >
        <OrbitControls />
        <Suspense fallback={<Loader />}>
          <Sampler dotCount={dotCount} setDotCount={setDotCount} />
        </Suspense>
      </Canvas>
    </StyledTHREEContainer>
  );
};
问题解答

问题1:优化Sampler组件重复渲染

核心问题

  • 全局变量pos/cols作为状态容器,每次dotCount变化时生成新的Float32Array,触发组件重渲染。
  • whaleMaterials、samplerWhale等实例在每次渲染时重复创建,产生不必要的计算开销。
  • 空的useFrame会强制组件在每一帧更新,加剧渲染频率。

优化方案

  1. 用组件内部状态替代全局变量:通过useState管理点的位置和颜色数组,避免全局状态的不可控更新。
  2. 缓存静态资源与实例:用useMemo缓存palette、whaleMaterials、samplerWhale等不会频繁变化的内容,减少重复创建。
  3. 移除无用的useFrame:无动画逻辑时直接删除,避免强制帧更新。
  4. 增量更新点数据:不要每次重建整个Float32Array,仅在dotCount变化时追加新数据,再缓存最终的数组实例。

修改后的核心代码片段:

const Sampler = ({ dotCount, setDotCount }) => {
  // 内部状态管理点数据
  const [positionsArray, setPositionsArray] = useState([]);
  const [colorsArray, setColorsArray] = useState([]);

  // 缓存静态调色板
  const palette = useMemo(() => [
    new THREE.Color("#FAAD80"),
    new THREE.Color("#FF6767"),
    new THREE.Color("#FF3D68"),
    new THREE.Color("#A73489"),
  ], []);

  const tempPosition = useMemo(() => new Vector3(), []);
  const colorMap = useLoader(TextureLoader, "https://assets.codepen.io/127738/dotTexture.png");
  const whaleMaterials = useMemo(() => new THREE.MeshBasicMaterial({
    wireframe: true,
    color: 0x000000,
    transparent: true,
    opacity: 0.05,
  }), []);
  const obj = useLoader(OBJLoader, "/textures/whale/Mesh_Whale.obj");
  
  // 缓存采样器实例
  const samplerWhale = useMemo(() => {
    obj.children[0].material = whaleMaterials;
    const sampler = new MeshSurfaceSampler(obj.children[0]);
    sampler.build();
    return sampler;
  }, [obj, whaleMaterials]);

  // 仅在dotCount变化时添加新点
  useEffect(() => {
    samplerWhale.sample(tempPosition);
    setPositionsArray(prev => [...prev, tempPosition.x, tempPosition.y, tempPosition.z]);
    const randomColor = palette[Math.floor(Math.random() * palette.length)];
    setColorsArray(prev => [...prev, randomColor.r, randomColor.g, randomColor.b, 1]);
  }, [dotCount, samplerWhale, tempPosition, palette]);

  // 缓存最终的Float32Array
  const positions = useMemo(() => new Float32Array(positionsArray), [positionsArray]);
  const colors = useMemo(() => new Float32Array(colorsArray), [colorsArray]);

  useEffect(() => {
    const t = setInterval(() => {
      if (dotCount < 100) {
        setDotCount(prev => prev + 1);
      } else {
        clearInterval(t);
      }
    }, 100);
    return () => clearInterval(t);
  }, [dotCount, setDotCount]);

  // 返回部分保持不变
};

问题2:Suspense报错原因与解决

报错原因

该错误是因为在组件渲染阶段触发了父组件的状态更新:

  • Sampler组件的useEffect依赖项为空数组,会在组件首次渲染时执行,此时Suspense仍在显示Loader组件。
  • 调用setDotCount会触发父组件SurfaceSampling的状态更新,而React禁止在渲染一个组件的过程中更新另一个组件的状态(此时Loader正处于渲染状态)。

解决方法

  1. 完善useEffect依赖项:将obj、dotCount、setDotCount加入依赖数组,确保在模型加载完成后再执行定时器逻辑,避免在Suspense加载阶段触发状态更新。
  2. 终止定时器条件:当dotCount达到100时主动清除定时器,避免无效循环。

修改后的useEffect代码:

useEffect(() => {
  if (!obj) return;
  const t = setInterval(() => {
    if (dotCount < 100) {
      setDotCount(prev => prev + 1);
    } else {
      clearInterval(t);
    }
  }, 100);
  return () => clearInterval(t);
}, [obj, dotCount, setDotCount]);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 12:24:21