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

滚动时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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 05:45:21