React集成Three.js问题:组件重渲染与Suspense报错排查
问题描述
我正在尝试将原生Three.js项目迁移到React,当前遇到两个问题:
- 每次生成新的点时,Sampler组件都会重新渲染,该如何优化?
- 使用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会强制组件在每一帧更新,加剧渲染频率。
优化方案
- 用组件内部状态替代全局变量:通过
useState管理点的位置和颜色数组,避免全局状态的不可控更新。 - 缓存静态资源与实例:用
useMemo缓存palette、whaleMaterials、samplerWhale等不会频繁变化的内容,减少重复创建。 - 移除无用的
useFrame:无动画逻辑时直接删除,避免强制帧更新。 - 增量更新点数据:不要每次重建整个
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正处于渲染状态)。
解决方法
- 完善
useEffect依赖项:将obj、dotCount、setDotCount加入依赖数组,确保在模型加载完成后再执行定时器逻辑,避免在Suspense加载阶段触发状态更新。 - 终止定时器条件:当
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
相关产品推荐
相关产品推荐

