滚动时React Three Fiber组件重复渲染问题排查求助
滚动时FBO粒子组件重复渲染问题排查
我用Shader和FBO构建粒子场景,向下滚动时组件出现重复渲染问题。尝试过用useRef、useMemo替代useState避免重渲染,也试过useGLTF加载3D模型,都没解决。以下是代码,求排查原因:
代码示例
let opacity; const size = 80; const FboParticles = ({ models }) => { const { viewport } = useThree(); const scroll = useScroll(); /** * Particles options */ // This reference gives us direct access to our points const points = useRef(); const simulationMaterialRef = useRef(); // Create a camera and a scene for our FBO // Create a simple square geometry with custom uv and positions attributes const [scene, camera, positions, uvs] = useMemo(() => { return [ new Scene(), new OrthographicCamera(-1, 1, 1, -1, 1 / Math.pow(2, 53), 1), new Float32Array([ -1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0, ]), new Float32Array([0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0]), ]; }, []); // Create our FBO render target const renderTarget = useFBO(size, size, { minFilter: NearestFilter, magFilter: NearestFilter, format: RGBAFormat, stencilBuffer: false, type: FloatType, }); // Generate a "buffer" of vertex of size "size" with normalized coordinates const particlesPosition = useMemo(() => { const length = size * size; const particles = new Float32Array(length * 3); for (let i = 0; i < length; i++) { let i3 = i * 3; particles[i3 + 0] = (i % size) / size; particles[i3 + 1] = i / size / size; } return particles; }, []); const uniforms = useMemo( () => ({ uPositions: { value: null, }, uMobile: { value: 1, }, uPixelRatio: { value: Math.min(window.devicePixelRatio, 2), }, vColor: { value: 0, }, defaultTime: { value: 0, }, uOpacity: { value: 0, }, uColorTrigger: { value: 0, }, }), [] ); useLayoutEffect(() => { if (viewport.width < 4.8) { points.current.material.uniforms.uMobile.value = 0.1; } }, []); // FBO useFrame useFrame(({ gl, clock }) => { gl.setRenderTarget(renderTarget); gl.clear(); gl.render(scene, camera); gl.setRenderTarget(null); points.current.material.uniforms.uPositions.value = renderTarget.texture; points.current.material.uniforms.defaultTime.value = clock.elapsedTime; simulationMaterialRef.current.uniforms.uTime.value = clock.elapsedTime; }); const [scales, colors] = useMemo(() => { const length = size * size * 3; const color = new Color(); const sca = []; const cols = []; let q = ["white", "white", 0x2675ad, 0x0b5394, 0x0b9490]; const range = viewport.width < 4.8 ? 20 : 40; for (let i = 0; i < length; i++) { const i3 = i * 3; // color color.set(q[randInt(0, 4)]); cols[i3 + 0] = color.r; cols[i3 + 1] = color.g; cols[i3 + 2] = color.b; // particles scale sca[i] = Math.random() * range; } return [new Float32Array(sca), new Float32Array(cols)]; }, []); /** * mouse event */ const hoveredRef = useRef(); const planeGeo = useMemo(() => { return new PlaneGeometry(viewport.width, viewport.height, 1, 1); }, []); /** * scroll */ // scroll animation useFrame(({ mouse }) => { const x = (mouse.x * viewport.width) / 2; const y = (mouse.y * viewport.height) / 2; if (viewport.width > 6.7) { if (hoveredRef.current) { simulationMaterialRef.current.uniforms.uMouse.value = new Vector2(x, y); simulationMaterialRef.current.uniforms.uMouseTrigger.value = 1; } else simulationMaterialRef.current.uniforms.uMouseTrigger.value = 0; } const aRange = scroll.range(0.0, 1 / 12); const bRange = scroll.range(0.7 / 12, 1 / 12); simulationMaterialRef.current.uniforms.scrollTriggerA.value = aRange; const cRange = scroll.range(1.7 / 12, 1 / 12); const dRange = scroll.range(3.6 / 12, 1 / 12); const c = scroll.visible(1.7 / 12, 1 / 12); const d = scroll.visible(2.7 / 12, 1.9 / 12); simulationMaterialRef.current.uniforms.scrollTriggerB.value = cRange; const e = scroll.visible(4.8 / 12, 2 / 12); const eRange = scroll.range(4.8 / 12, 1 / 12); simulationMaterialRef.current.uniforms.scrollTriggerC.value = eRange; const f = scroll.visible(6.8 / 12, 1 / 12); const g = scroll.visible(7.6 / 12, 1 / 12); const fRange = scroll.range(6.8 / 12, 1 / 12); const gRange = scroll.range(7.6 / 12, 1 / 12); simulationMaterialRef.current.uniforms.scrollTriggerD.value = fRange; simulationMaterialRef.current.uniforms.scrollTriggerE.value = gRange; const h = scroll.visible(9.6 / 12, 2.4 / 12); const hRange = scroll.range(9.6 / 12, 1 / 12); simulationMaterialRef.current.uniforms.scrollTriggerF.value = hRange; points.current.material.uniforms.uColorTrigger.value = hRange; const iRange = scroll.range(10.6 / 12, 1.4 / 12); simulationMaterialRef.current.uniforms.scrollTriggerG.value = iRange; // opacity opacity = 1 - bRange; c && (opacity = cRange); d && (opacity = 1 - dRange); e && (opacity = eRange); f && (opacity = 1 - fRange); g && (opacity = fRange); h && (opacity = hRange); points.current.material.uniforms.uOpacity.value = opacity; }); return ( <> <mesh position={[0, 0, 0]} onPointerOver={(e) => ( e.stopPropagation(), (hoveredRef.current = true) )} onPointerOut={(e) => (hoveredRef.current = false)} visible={false} geometry={planeGeo} /> {/* Render off-screen our simulation material and square geometry */} {createPortal( <mesh> <simulationMaterial ref={simulationMaterialRef} args={[size, viewport, models]} /> <bufferGeometry> <bufferAttribute attach="attributes-position" count={positions.length / 3} array={positions} itemSize={3} /> <bufferAttribute attach="attributes-uv" count={uvs.length / 2} array={uvs} itemSize={2} /> </bufferGeometry> </mesh>, scene )} <points ref={points}> <bufferGeometry> <bufferAttribute attach="attributes-position" count={particlesPosition.length / 3} array={particlesPosition} itemSize={3} /> <bufferAttribute attach="attributes-color" count={colors.length / 3} array={colors} itemSize={3} /> <bufferAttribute attach="attributes-aScale" count={scales.length} array={scales} itemSize={1} /> </bufferGeometry> <shaderMaterial blending={AdditiveBlending} depthWrite={false} // transparent={true} fragmentShader={fragmentShader} vertexShader={vertexShader} uniforms={uniforms} /> </points> </> ); }; export default FboParticles;
可能的问题点及修复建议
1. 依赖项缺失导致的无效缓存
多个useMemo和useLayoutEffect的依赖数组为空,但内部用到了viewport这类会变化的变量:
scales/colors的useMemo依赖viewport.width,需添加[viewport.width, size]planeGeo的useMemo依赖viewport.width/height,需添加[viewport.width, viewport.height]useLayoutEffect依赖viewport.width,需添加[viewport.width],否则窗口resize后不会更新uMobile值
2. 全局变量opacity的副作用
把opacity定义在组件外部,滚动时修改会污染全局状态,导致渲染逻辑混乱。直接在useFrame内部计算并赋值:
let opacity = 1 - bRange; if (c) opacity = cRange; if (d) opacity = 1 - dRange; // ... 其他条件判断 points.current.material.uniforms.uOpacity.value = opacity;
3. 多个useFrame的冗余执行
代码中有两个独立的useFrame钩子,每一帧都会重复执行。合并为一个,减少开销:
useFrame(({ gl, clock, mouse }) => { // FBO渲染逻辑 gl.setRenderTarget(renderTarget); gl.clear(); gl.render(scene, camera); gl.setRenderTarget(null); points.current.material.uniforms.uPositions.value = renderTarget.texture; points.current.material.uniforms.defaultTime.value = clock.elapsedTime; simulationMaterialRef.current.uniforms.uTime.value = clock.elapsedTime; // 滚动与鼠标逻辑 const x = (mouse.x * viewport.width) / 2; const y = (mouse.y * viewport.height) / 2; if (viewport.width > 6.7) { if (hoveredRef.current) { simulationMaterialRef.current.uniforms.uMouse.value.set(x, y); simulationMaterialRef.current.uniforms.uMouseTrigger.value = 1; } else simulationMaterialRef.current.uniforms.uMouseTrigger.value = 0; } // ... 其他滚动相关计算 });
4. 频繁创建Vector2对象
每次鼠标移动都创建新Vector2,会产生不必要的垃圾回收。初始化时用useRef保存一个实例,后续用set更新:
const mouseVectorRef = useRef(new Vector2()); // 在useFrame中 mouseVectorRef.current.set(x, y); simulationMaterialRef.current.uniforms.uMouse.value = mouseVectorRef.current;
内容的提问来源于stack exchange,提问作者hello
相关产品推荐
相关产品推荐

