React Three Fiber sin jank: cómo medir y optimizar escenas WebGL

Una guía práctica para medir el frame budget y optimizar draw calls, React, DPR, texturas, sombras, shaders y memoria en escenas WebGL.

10 min

React Three Fiber performance: frame budget, draw calls and GPU

Una escena 3D fluida no se consigue acumulando trucos, sino administrando un presupuesto. Cada cuadro debe completar trabajo de JavaScript, actualización de la escena y dibujo en la GPU antes de que llegue el siguiente refresco de pantalla.

En una pantalla de 60 Hz ese margen ronda los 16,7 ms; a 120 Hz se reduce a 8,3 ms. No son objetivos universales: el dispositivo, la complejidad visual y el tipo de interacción determinan el presupuesto real.

Optimizar React Three Fiber significa identificar qué parte del cuadro consume el presupuesto y reducir ese costo sin degradar la intención visual.

01. Empieza por el presupuesto del cuadro

Anatomía del presupuesto de un cuadro WebGL con trabajo de CPU, render y GPUAnatomía del presupuesto de un cuadro WebGL con trabajo de CPU, render y GPU

Un FPS bajo no explica la causa. El cuello de botella puede estar en lugares distintos:

SeñalCausa probablePrimera comprobación
JavaScript tarda demasiadocálculos, allocations o renders de ReactPerformance de DevTools
muchos draw callsdemasiados objetos o materiales separadosgl.info.render.calls
demasiados triángulosgeometría más densa de lo necesariogl.info.render.triangles
la GPU se satura al subir DPRfill rate, sombras o postprocesadocomparar DPR y resolución
la memoria crece al navegartexturas, materiales o geometrías sin liberargl.info.memory

React Three Fiber expone el renderer de Three.js mediante useThree. Durante desarrollo puedes muestrear sus contadores sin actualizar estado de React en cada cuadro:

tsx
function RendererProbe() {
  const gl = useThree((state) => state.gl);
  const lastReport = useRef(0);

  useFrame(() => {
    const now = performance.now();
    if (now - lastReport.current < 1000) return;
    lastReport.current = now;

    console.table({
      calls: gl.info.render.calls,
      triangles: gl.info.render.triangles,
      geometries: gl.info.memory.geometries,
      textures: gl.info.memory.textures,
    });
  });

  return null;
}

Úsalo como sonda temporal, no como telemetría de producción. Combina esos números con el perfil del navegador y prueba en un dispositivo representativo; el portátil de desarrollo rara vez es el límite real.

02. Reduce draw calls antes de reducir detalle

Comparación visual entre cientos de meshes individuales y un solo InstancedMeshComparación visual entre cientos de meshes individuales y un solo InstancedMesh

La GPU puede procesar muchos vértices, pero cada draw call requiere coordinación entre CPU y GPU. Cientos de objetos con la misma geometría y material son buenos candidatos para InstancedMesh.

tsx
function Field({ count = 1000 }) {
  const mesh = useRef<THREE.InstancedMesh>(null);
  const transform = useMemo(() => new THREE.Object3D(), []);

  useLayoutEffect(() => {
    if (!mesh.current) return;

    for (let index = 0; index < count; index += 1) {
      transform.position.set(
        (index % 40) - 20,
        0,
        Math.floor(index / 40) - 12,
      );
      transform.updateMatrix();
      mesh.current.setMatrixAt(index, transform.matrix);
    }

    mesh.current.instanceMatrix.needsUpdate = true;
    mesh.current.computeBoundingSphere();
  }, [count, transform]);

  return (
    <instancedMesh ref={mesh} args={[undefined, undefined, count]}>
      <boxGeometry args={[0.18, 0.18, 0.18]} />
      <meshStandardMaterial color="#6d4aff" />
    </instancedMesh>
  );
}

El instancing funciona cuando las instancias comparten geometría y material. Para objetos estáticos diferentes, considera unir geometrías compatibles. También comparte materiales y geometrías en lugar de recrearlos dentro de cada componente.

No optimices solo por el número de objetos: un único mesh con un shader costoso o millones de triángulos todavía puede saturar la GPU.

03. Mantén el trabajo por cuadro fuera de React

useFrame se ejecuta en el loop de render. Llamar setState allí puede provocar reconciliaciones al ritmo de la pantalla. Para animaciones de alta frecuencia, modifica referencias de Three.js y reserva el estado de React para cambios semánticos de la interfaz.

tsx
function Rotor({ speed = 0.8 }) {
  const group = useRef<THREE.Group>(null);

  useFrame((_, delta) => {
    if (group.current) {
      group.current.rotation.y += speed * delta;
    }
  });

  return <group ref={group}>{/* scene content */}</group>;
}

Usar delta mantiene la velocidad independiente de los FPS. Evita además crear vectores, colores o arrays dentro del loop; reutiliza objetos o calcula datos estables con useMemo. Una pequeña allocation repetida miles de veces termina convertida en pausas del recolector de basura.

04. No renderices cuadros que nadie puede ver

Una escena animada necesita frameloop="always", el valor habitual. Un configurador, un modelo de producto o una visualización que solo cambia con la interacción puede trabajar bajo demanda:

tsx
function ProductViewer() {
  return (
    <Canvas frameloop="demand" dpr={[1, 1.5]}>
      <Scene />
    </Canvas>
  );
}

function MaterialSync({ color }: { color: string }) {
  const material = useRef<THREE.MeshStandardMaterial>(null);
  const invalidate = useThree((state) => state.invalidate);

  useEffect(() => {
    material.current?.color.set(color);
    invalidate();
  }, [color, invalidate]);

  return <meshStandardMaterial ref={material} />;
}

Los cambios declarativos gestionados por React Three Fiber solicitan cuadros cuando corresponde. Si mutas un objeto de forma imperativa, llama invalidate() para programar el siguiente. No mezcles render bajo demanda con una animación continua sin definir quién despierta el loop.

05. Controla el costo de cada píxel

Panel técnico de calidad WebGL con controles de DPR, sombras, texturas y postprocesadoPanel técnico de calidad WebGL con controles de DPR, sombras, texturas y postprocesado

Duplicar el DPR puede aproximarse a cuadruplicar la cantidad de píxeles. Por eso una escena fluida en una pantalla estándar puede caer en una pantalla de alta densidad aunque tenga los mismos draw calls.

Ordena las decisiones de calidad por impacto:

  1. limita el DPR a un rango razonable;
  2. reduce resolución y cantidad de shadow maps;
  3. limita luces que proyectan sombras y objetos que las reciben;
  4. comprime y dimensiona texturas según su uso visible;
  5. elimina pases de postprocesado cuyo aporte no justifica otro render completo;
  6. usa niveles de detalle para objetos lejanos.

Las texturas suelen dominar memoria y ancho de banda. Una imagen grande no se vuelve barata porque ocupe pocos kilobytes comprimida en la red: en la GPU se expande a una representación apta para muestreo. Ajusta dimensiones, formato y mipmaps al caso real.

06. Shaders: mover trabajo no elimina el costo

Un shader puede reemplazar miles de actualizaciones de JavaScript por cálculo paralelo en la GPU. Es ideal para ondas, partículas y deformaciones, pero no es una licencia para hacer trabajo ilimitado por vértice o por fragmento.

glsl
uniform float uTime;
attribute float phase;

void main() {
  vec3 displaced = position;
  displaced.y += sin(uTime + phase) * 0.08;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);
}

Actualiza uniforms existentes en lugar de reconstruir materiales. Evita multiplicar variantes de shader mediante defines cambiantes: cada combinación puede requerir otro programa y una nueva compilación. Mide por separado escenas limitadas por vértices y escenas limitadas por fill rate.

07. Libera recursos y optimiza en orden

Secuencia editorial para diagnosticar y optimizar una escena React Three FiberSecuencia editorial para diagnosticar y optimizar una escena React Three Fiber

Three.js no puede liberar automáticamente todos los recursos de GPU cuando desaparece una referencia de JavaScript. Los objetos creados manualmente fuera del reconciliador —o conservados en caches propias— necesitan una estrategia de propiedad y llamadas a dispose() para geometrías, materiales, texturas y render targets.

Antes de considerar terminada una optimización, sigue este orden:

  • fija un dispositivo, una escena y un objetivo medible;
  • identifica si el límite está en CPU, draw calls, geometría, píxeles o memoria;
  • reduce trabajo estructural: renders de React, objetos, materiales y draw calls;
  • ajusta DPR, sombras, texturas y postprocesado;
  • elimina allocations del loop y estabiliza recursos;
  • optimiza shaders solo cuando el perfil señale a la GPU;
  • repite la medición y compara la calidad visual.

El mejor resultado no es el FPS más alto en una escena vacía. Es una experiencia consistente que conserva su jerarquía visual dentro del presupuesto de los dispositivos que realmente la ejecutan.


SESSION_ELAPSED00:00:00
LOCALE: ESENV: PROD