SYS // ARTICLES // WEBGL_PERFORMANCE_REACT_THREE_FIBER
articles/Experiments

Optimizing 3D Scenes with React Three Fiber

How to render thousands of 3D objects at 60 FPS using instanced meshes, demand-driven frameloops, and custom shaders in GLSL.

Interactive 3D simulation with WebGL and React Three Fiber

High-Performance 3D Rendering on the Web

In the era of high-impact frontend development, integrating interactive three-dimensional experiences has become a signature mark for tech-focused brands. WebGL, through libraries like Three.js and its React-wrapper, React Three Fiber (R3F), allows developers to build complex scenes that previously required dedicated desktop engines.

However, moving to 3D on the web comes with critical performance challenges. Maintaining a fluid refresh rate of 60 FPS (or 120 FPS on modern displays) requires understanding how the CPU (via Draw Calls) and the GPU (via shaders and instancing) interact.

The Draw Call Bottleneck

Every time you request the graphics card to render an object, a draw call is generated. If your scene contains 1,000 independent particles or cubes, the CPU will send 1,000 separate commands to the GPU. This overhead clogs the communication channel, causing severe frame drops.

The Instancing Solution

The most efficient solution to this issue is Instancing. By using Three.js's InstancedMesh, you can send a single draw command to the GPU along with a buffer containing transformation matrices. This tells the GPU: "Render this identical geometry 1,000 times, but place each one at these specific coordinates."

tsx
import { useRef, useEffect } from 'react';
import * as THREE from 'three';

export function InstancedBoxes({ count = class="text-amber-600 dark:text-[#ffd385]">1000 }) {
  const meshRef = useRef<THREE.InstancedMesh>(null);
  const tempObject = new THREE.Object3D();

  useEffect(() => {
    if (!meshRef.current) return;
    
    // Position each instance randomly in a 3D grid
    for (let i = class="text-amber-600 dark:text-[#ffd385]">0; i < count; i++) {
      tempObject.position.set(
        (Math.random() - class="text-amber-600 dark:text-[#ffd385]">0.5) * class="text-amber-600 dark:text-[#ffd385]">10,
        (Math.random() - class="text-amber-600 dark:text-[#ffd385]">0.5) * class="text-amber-600 dark:text-[#ffd385]">10,
        (Math.random() - class="text-amber-600 dark:text-[#ffd385]">0.5) * class="text-amber-600 dark:text-[#ffd385]">10
      );
      tempObject.updateMatrix();
      meshRef.current.setMatrixAt(i, tempObject.matrix);
    }
    
    meshRef.current.instanceMatrix.needsUpdate = class="text-amber-600 dark:text-[#ffd385]">true;
  }, [count]);

  return (
    <instancedMesh ref={meshRef} args={[null, null, count]}>
      <boxGeometry args={[class="text-amber-600 dark:text-[#ffd385]">0.2, class="text-amber-600 dark:text-[#ffd385]">0.2, class="text-amber-600 dark:text-[#ffd385]">0.2]} />
      <meshStandardMaterial color="#06b6d4" />
    </instancedMesh>
  );
}

Frameloop Control and Custom Shaders

Another typical bottleneck happens when React Three Fiber renders the canvas continuously. By default, R3F renders at the maximum frequency available in the main loop. If your scene is mostly static and only reacts to user events, you can switch the frameloop mode to demand:

tsx
<Canvas frameloop="demand">
  <ambientLight />
  <mesh onClick={() => /* update state and trigger frame */ {}} />
</Canvas>

Additionally, delegating math-heavy animations (like sine wave movement or noise offsets) directly to GLSL Vertex Shaders completely frees the CPU from computing coordinates on every single frame. The GPU is exceptionally good at running these algebraic calculations in parallel, instantly.

SESSION_ELAPSED: 00:00:00:00
LOCALE: EN//ENV: PROD