wandres.dev
ONTOLOGÍA · El mapa del 3D en la web

WebGL2 y WebGPU: dos contratos, no dos versiones

Qué cambia de verdad entre las dos APIs gráficas de la web, por qué WebGPU no es WebGL rápido, y cómo decide Three.js cuál usar sin que tu código se entere.

⏱ 18 min

Es tentador leer WebGPU como la versión nueva de WebGL, con mejor rendimiento y sintaxis más limpia. Es una lectura equivocada y produce decisiones malas. WebGPU rediseña el modelo de trabajo: cambia dónde ocurre la validación, cómo se agrupan los recursos y qué puede hacer la GPU sin pasar por el rasterizador. Three.js absorbe casi todo ese cambio, pero no todo, y saber qué queda al descubierto es lo que separa elegir con criterio de elegir por titular.

🎯 Al terminar esta lección sabrás
  • Explicar la diferencia estructural entre el modelo de estado global de WebGL y el modelo de objetos precocinados de WebGPU.
  • Justificar por qué WebGPU reduce el coste de CPU por orden de dibujo.
  • Enumerar qué gana una aplicación de Three.js al cambiar de renderer y qué no.
  • Elegir renderer con un criterio de cobertura de dispositivos, no de novedad.

El modelo de estado global contra el modelo precocinado

WebGL hereda de OpenGL ES una idea de los años noventa: una máquina de estados global. Enlazas un búfer al punto de enlace de arrays, enlazas una textura a la unidad cero, activas un programa, ajustas la función de mezcla y luego dices «dibuja». La orden de dibujo no lleva parámetros: opera sobre el estado que dejaste puesto. Es cómodo de aprender y terrible de validar, porque cada llamada puede invalidar cualquier suposición previa y la implementación tiene que comprobarlo todo cada vez.

WebGPU invierte el planteamiento. Antes de dibujar, construyes objetos inmutables que describen configuraciones completas: un pipeline de render encapsula los shaders, el formato de los vértices, el estado de profundidad, el de mezcla y el de rasterización, todo junto y validado una sola vez en el momento de crearlo. Los recursos se agrupan en bind groups, también validados al crearse. En tiempo de dibujo solo dices «usa este pipeline, este bind group, dibuja». Prácticamente no queda nada que validar.

Ese es el cambio de fondo, y de él salen las consecuencias reales:

La validación se paga una vez, no por fotograma. En WebGL, cada llamada cruza una frontera de proceso y una batería de comprobaciones. En WebGPU, los comandos se graban en un command encoder y se envían en lote. El coste de CPU por orden de dibujo cae de forma drástica, y eso importa exactamente en el escenario donde la web sufre: escenas con muchos objetos distintos.

Aparece el cómputo de propósito general. WebGL no tiene compute shaders. Para hacer cálculo en GPU había que disfrazarlo de dibujado sobre una textura, con toda la torpeza que eso implica. WebGPU tiene compute shaders de primera clase y búferes de almacenamiento con escritura, lo que abre simulación de partículas, física en GPU y post-procesado en formas que en WebGL eran acrobacias.

El lenguaje de shaders cambia. GLSL ES para WebGL, WGSL para WebGPU. WGSL tiene sintaxis inspirada en Rust, tipado explícito y reglas de alineación de memoria más estrictas. No es una traducción mecánica del GLSL.

El modelo de errores cambia. WebGPU es asíncrono desde la raíz: pedir un adaptador y un dispositivo devuelve promesas, y los errores llegan por ámbitos de error en lugar de por un getError global.

Qué hace Three.js con todo esto

Aquí está la parte que ahorra trabajo. Three.js mantiene dos renderers: WebGLRenderer, el clásico, y WebGPURenderer, construido sobre una arquitectura de nodos. La API de escena es la misma: la misma Scene, la misma PerspectiveCamera, las mismas geometrías. Lo que cambia es el sistema de materiales y de shaders.

Y ese cambio se resuelve con TSL, el lenguaje de shading de Three.js, que en la revisión r184 es una API de primera clase. En lugar de escribir GLSL o WGSL, describes un grafo de nodos en JavaScript, y el motor lo compila a GLSL cuando el backend es WebGL y a WGSL cuando es WebGPU. Un solo código fuente para los dos destinos. Los materiales de nodos son MeshStandardNodeMaterial y su familia, y los puntos de extensión son material.colorNode, material.positionNode y material.normalNode.

Además, WebGPURenderer cae automáticamente a WebGL cuando el navegador no expone WebGPU. No hay dos rutas de código en tu aplicación: hay una, y el backend se elige en tiempo de ejecución.

import * as THREE from 'three/webgpu';

const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// La inicializacion es asincrona: aqui se elige y se prepara el backend.
await renderer.init();

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  50,
  window.innerWidth / window.innerHeight,
  0.1,
  100
);
camera.position.set(0, 1, 4);

const malla = new THREE.Mesh(
  new THREE.TorusKnotGeometry(0.7, 0.25, 128, 32),
  new THREE.MeshStandardNodeMaterial({ color: 0x89b4fa, roughness: 0.3, metalness: 0.6 })
);
scene.add(malla);

const luz = new THREE.DirectionalLight(0xffffff, 3);
luz.position.set(2, 3, 4);
scene.add(luz, new THREE.AmbientLight(0xffffff, 0.4));

renderer.setAnimationLoop(() => {
  malla.rotation.y += 0.01;
  renderer.render(scene, camera);
});

Tres detalles de ese código merecen atención. El primero es el import: three/webgpu es un punto de entrada distinto de three, y expone el renderer moderno junto con los materiales de nodos. El segundo es await renderer.init(), que no existe en WebGLRenderer y refleja la naturaleza asíncrona de WebGPU. El tercero es que renderer.render con este renderer encola trabajo; existe también renderer.renderAsync cuando necesitas esperar a que termine de verdad.

Si quieres forzar el backend clásico para comparar, el constructor acepta forceWebGL:

import * as THREE from 'three/webgpu';

const usarWebGL = new URLSearchParams(location.search).has('webgl');
const renderer = new THREE.WebGPURenderer({ antialias: true, forceWebGL: usarWebGL });
await renderer.init();
console.log('backend forzado a WebGL:', usarWebGL);

Ese interruptor por parámetro de URL vale su peso en oro durante el desarrollo: te deja reproducir en un segundo el comportamiento que verá un usuario sin WebGPU.

El fallback automático no significa resultado idéntico

Que WebGPURenderer caiga a WebGL sin que tu código cambie es una comodidad enorme, pero es fácil leerla como una garantía que nadie ha dado. Lo que se garantiza es que la escena se dibuja; no que se dibuje igual ni que rinda igual. Todo lo que dependa de capacidades exclusivas de WebGPU —cómputo con búferes de almacenamiento, escritura arbitraria desde un shader, ciertos formatos de textura— no tiene equivalente en el backend WebGL, y el resultado va desde una degradación silenciosa hasta un error en consola. Además, el mismo grafo TSL compilado a GLSL y a WGSL puede diferir en los bordes: el manejo de precisión, el redondeo y el orden de operaciones no están obligados a coincidir bit a bit. La disciplina que evita sorpresas es simple y casi nadie la aplica: si publicas con el fallback activo, prueba con el fallback activo en cada iteración, no solo al final. Un interruptor como el de arriba y una pasada semanal por la ruta WebGL detectan en minutos lo que si no se descubre en producción, en el dispositivo de un usuario que no puedes depurar.

Cómo elegir en 2026 sin equivocarse

WebGPU está disponible en los tres motores de navegador por defecto desde que Safari 26 lo implementó en septiembre de 2025, en macOS, iPadOS, iOS y visionOS. Eso significa que la pregunta ya no es si existe, sino qué porcentaje del parque de dispositivos de tu público lo tiene y qué haces con el resto.

Y ahí el matiz importa: WebGL2 sigue siendo la base más segura por cobertura de dispositivos antiguos. Un móvil de gama media de hace cinco años ejecuta WebGL2 y probablemente no ejecute WebGPU. El enfoque correcto no es sustituir sino mejorar progresivamente: escribe contra WebGPURenderer con TSL, deja que el fallback cubra el resto, y diseña la escena para que funcione con el conjunto de capacidades del denominador común.

El criterio de decisión, en orden:

Si empiezas un proyecto hoy y no tienes una restricción concreta, usa WebGPURenderer con materiales de nodos y TSL. Es la dirección del desarrollo de la biblioteca, el fallback te cubre y no estás pagando complejidad extra.

Si necesitas cómputo en GPU —simulación de partículas a gran escala, física, cualquier cosa que quiera leer y escribir búferes desde un shader— entonces WebGPU no es una preferencia, es un requisito, y tienes que decidir qué experiencia degradada ofreces a quien no lo tenga.

Si mantienes una base de código grande sobre WebGLRenderer con shaders GLSL propios y onBeforeCompile, migrar no es gratis: los shaders hay que reescribirlos en TSL o mantener dos versiones. Migra cuando haya un motivo, no por higiene.

Si tu público es mayoritariamente móvil de gama baja, el cuello de botella no lo va a resolver la API. Lo va a resolver reducir píxeles, triángulos y complejidad de sombreado, y eso se hace igual en las dos.

En la ontología del territorio verás dónde encajan TSL y el renderer moderno dentro del recorrido completo; antes conviene mirar qué otras bibliotecas existen, porque la elección de API y la elección de motor son decisiones distintas que la gente suele mezclar.