Saltar al contenido

Cuando no hay WebGL: detección, contexto perdido y fallback

Cómo comprobar el soporte antes de construir nada, qué hacer cuando el navegador te quita el contexto en mitad de la sesión, y cómo diseñar un sustituto que no sea un rectángulo gris.

Comprobar antes de construir

El addon de capacidades trae la comprobación hecha, y hace exactamente lo mínimo necesario: crea un canvas suelto, intenta obtener un contexto de WebGL 2 y devuelve un booleano.

JS
import WebGL from 'three/addons/capabilities/WebGL.js';if (!WebGL.isWebGL2Available()) {  const aviso = WebGL.getWebGL2ErrorMessage();  contenedor.appendChild(aviso);} else {  arrancarEscena();}

getWebGL2ErrorMessage devuelve un div con un mensaje en inglés y estilos en línea. Sirve para una demo; en producción querrás tu propio mensaje, en tu idioma y con tu diseño, y sobre todo con contenido alternativo real en lugar de una disculpa.

Un matiz sobre el coste: el canvas de prueba crea un contexto que cuenta para el límite de contextos simultáneos del navegador hasta que se recoge. En la práctica es intrascendente si compruebas una vez, pero comprobar en un bucle o en cada componente es una forma sorprendentemente eficaz de agotar el límite.

Si construyes el renderer directamente sin comprobar, el constructor lanza cuando no puede obtener contexto. Envolver en un try es una alternativa válida y evita el contexto de prueba:

JS
let renderer = null;try {  renderer = new THREE.WebGLRenderer({ antialias: true });} catch (e) {  mostrarAlternativa();}

failIfMajorPerformanceCaveat

Hay una opción del contexto que merece una decisión consciente. Con failIfMajorPerformanceCaveat: true, el navegador rechaza crear el contexto si va a usar un renderizador por software en lugar de la GPU.

JS
const renderer = new THREE.WebGLRenderer({  antialias: true,  failIfMajorPerformanceCaveat: true,});

Un renderizador por software funciona: dibuja correctamente. Lo que no hace es ir rápido. Una escena que va a sesenta fotogramas con GPU va a dos con SwiftShader, y ese es un resultado peor que no mostrar nada, porque además calienta la máquina y bloquea la interfaz.

El criterio es el tipo de contenido. Si tu 3D es la funcionalidad principal y no tiene sentido a dos fotogramas, activa la opción y ofrece la alternativa. Si tu 3D es un adorno que puede ir lento sin arruinar la página, déjala desactivada.

Hay una advertencia importante: el comportamiento de esta bandera no es idéntico entre navegadores, y algunos la ignoran en ciertas configuraciones. No la uses como única defensa; combínala con la medición de rendimiento de detectar la capacidad, que detecta el caso de todos modos.

El contexto perdido

Esta es la parte que casi ningún proyecto trata y que le ocurre a todos los usuarios. El navegador puede retirar el contexto WebGL en cualquier momento, y las causas son variadas y ninguna es culpa tuya: la GPU se ha reiniciado tras un cuelgue del controlador, el sistema ha entrado en suspensión, otra pestaña ha agotado la memoria de vídeo, o el usuario ha superado el límite de contextos simultáneos abriendo más pestañas con 3D.

Cuando ocurre, el canvas se queda como estaba en el último fotograma o en negro, y todas las llamadas de WebGL fallan silenciosamente. Sin manejadores, el usuario ve una escena congelada sin ninguna explicación.

JS
const lienzo = renderer.domElement;lienzo.addEventListener(  'webglcontextlost',  (evento) => {    // Sin preventDefault, el navegador NO intentara restaurar el contexto    evento.preventDefault();    renderer.setAnimationLoop(null);    mostrarAviso('Se ha interrumpido la aceleración gráfica. Restaurando…');  },  false,);lienzo.addEventListener(  'webglcontextrestored',  () => {    ocultarAviso();    // Los recursos de GPU se han perdido. Three vuelve a subir geometrias,    // texturas y programas la proxima vez que se usen, pero el estado    // del renderer hay que reponerlo.    reponerEstadoDelRenderer();    renderer.setAnimationLoop(frame);  },  false,);

La llamada a preventDefault en el evento de pérdida es obligatoria y contraintuitiva. Sin ella, el navegador da el contexto por perdido definitivamente y nunca emite el evento de restauración. Con ella, el navegador se compromete a restaurarlo cuando pueda.

Lo que hay que reponer tras la restauración es el estado que vive en el renderer y no en los objetos de la escena:

JS
function reponerEstadoDelRenderer() {  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));  renderer.setSize(contenedor.clientWidth, contenedor.clientHeight, false);  renderer.shadowMap.enabled = true;  renderer.toneMapping = THREE.ACESFilmicToneMapping;  renderer.toneMappingExposure = 1;  // Los render targets del post-proceso hay que redimensionarlos  composer?.setSize(contenedor.clientWidth, contenedor.clientHeight);}

Las geometrías, materiales y texturas se vuelven a subir solos: Three detecta que el recurso no está en la GPU y lo sube en el siguiente uso, porque los datos siguen en la memoria de JavaScript. Lo que no vuelve solo es lo que se generó en la GPU y no tiene copia en CPU: los mapas de entorno prefiltrados con PMREM, las texturas escritas por un render a textura, y cualquier simulación cuyo estado viva en un render target. Esos hay que regenerarlos explícitamente.

El sustituto que sí sirve

Un mensaje que dice “tu navegador no soporta WebGL” es un callejón sin salida. El usuario no puede hacer nada al respecto y se va. Lo que hay que ofrecer es el contenido, no una explicación de por qué falta.

Piensa en qué comunica tu escena 3D y cuál es la forma más simple de comunicar lo mismo. Un configurador de producto comunica cómo se ve el producto en cada combinación: la alternativa es una galería de fotos, una por combinación, generadas de antemano renderizando la misma escena. Una visualización de datos comunica una relación entre magnitudes: la alternativa es un gráfico plano o una tabla. Una experiencia narrativa comunica una historia: la alternativa es el texto y las imágenes.

HTML
<div class="visor" data-modo="alternativa">  <!-- Esto existe SIEMPRE en el HTML. El 3D lo sustituye si puede. -->  <picture>    <source srcset="/producto/frontal.avif" type="image/avif" />    <img      src="/producto/frontal.jpg"      alt="Silla Kompas en roble claro, vista de tres cuartos, con el asiento tapizado en lana gris."      width="1200" height="900"    />  </picture>  <div class="controles-alternativos">    <button data-vista="frontal" aria-pressed="true">Frontal</button>    <button data-vista="lateral" aria-pressed="false">Lateral</button>    <button data-vista="cenital" aria-pressed="false">Cenital</button>  </div></div>
JS
import WebGL from 'three/addons/capabilities/WebGL.js';const visor = document.querySelector('.visor');if (WebGL.isWebGL2Available()) {  // Solo entonces se sustituye la alternativa por el canvas  visor.dataset.modo = '3d';  arrancarEscena(visor);}

Este orden (alternativa primero, mejora después) es mejora progresiva aplicada al 3D, y tiene tres ventajas que van más allá del caso sin WebGL. Funciona sin JavaScript. Es lo que se ve mientras el motor descarga, así que elimina el hueco vacío durante la carga. Y es lo que indexan los buscadores y lo que leen los lectores de pantalla, que es lo que hace de esto una cuestión de accesibilidad además de robustez.

Las tres vistas del ejemplo se generan renderizando tu propia escena desde tres ángulos en el paso de construcción, con la misma iluminación y los mismos materiales. Un script de Node con Three.js sobre un canvas fuera de pantalla lo hace, o simplemente capturas manuales si el catálogo es pequeño. El resultado es coherente con el 3D porque es el 3D, congelado.

Un envoltorio que junta las tres defensas

JS
import * as THREE from 'three';import WebGL from 'three/addons/capabilities/WebGL.js';export function montarVisor(contenedor, { limitePrimerRenderMs = 1500 } = {}) {  if (!WebGL.isWebGL2Available()) {    contenedor.dataset.modo = 'alternativa';    return null;  }  let renderer;  try {    renderer = new THREE.WebGLRenderer({ antialias: true });  } catch {    contenedor.dataset.modo = 'alternativa';    return null;  }  const escena = construirEscena(renderer, contenedor);  const t0 = performance.now();  renderer.render(escena.scene, escena.camera);  const coste = performance.now() - t0;  if (coste > limitePrimerRenderMs) {    escena.destruir();    contenedor.dataset.modo = 'alternativa';    return null;  }  contenedor.dataset.modo = '3d';  conectarPerdidaDeContexto(renderer, escena);  renderer.setAnimationLoop(escena.frame);  return escena;}

Tres defensas encadenadas, cada una para un modo de fallo distinto: no hay contexto, el contexto falla al crearse, el contexto existe pero es inservible. Y en los tres casos el resultado es el mismo: el atributo del contenedor vuelve a alternativa y el CSS muestra el contenido que ya estaba ahí.

Que la degradación termine siempre en el mismo estado conocido es lo que hace el sistema mantenible. No hay tres caminos de error con tres mensajes distintos: hay un camino de éxito y un estado de reserva.