OffscreenCanvas: dibujar sin elemento
Usar el canvas que no vive en el DOM: crearlo, dibujar en él, convertirlo en imagen o en blob, y aprovecharlo como capa de caché en el hilo principal.
Crear y dibujar
const lienzo = new OffscreenCanvas(512, 512);const ctx = lienzo.getContext('2d');ctx.fillStyle = '#89b4fa';ctx.fillRect(0, 0, 512, 512);La API de dibujo es idéntica: el contexto es un OffscreenCanvasRenderingContext2D con exactamente los mismos métodos y propiedades. Todo lo aprendido hasta ahora se aplica sin cambios.
Lo que cambia es lo que rodea al dibujo:
No hay tamaño CSS. width y height son las únicas dimensiones que existen; no hay caja, no hay getBoundingClientRect, no hay densidad de pantalla implícita. Si quieres dibujar a la resolución del dispositivo, tienes que pasar tú el factor.
No hay toDataURL. En su lugar hay convertToBlob, que es asíncrono y admite opciones de formato y calidad.
Hay transferToImageBitmap. Extrae el contenido actual como ImageBitmap y deja el canvas vacío, transfiriendo la propiedad de los píxeles sin copiarlos.
// Blob para descargar o subirconst blob = await lienzo.convertToBlob({ type: 'image/webp', quality: 0.9 });// ImageBitmap para dibujar en otro sitio: sin copia, pero vacia el origenconst bitmap = lienzo.transferToImageBitmap();ctx.drawImage(bitmap, 0, 0); // en el canvas de destinobitmap.close();transferToImageBitmap es la operación más eficiente para mover el resultado de un canvas fuera de pantalla a otro sitio, precisamente porque no copia. El precio es que el origen queda en blanco, así que solo sirve cuando vas a redibujarlo entero de todos modos.
Como capa de caché
El uso más inmediato, sin workers de por medio, es sustituir el patrón de crear un elemento canvas para cachear algo costoso.
class CapaCache { constructor(ancho, alto, dpr = 1) { this.dpr = dpr; this.lienzo = new OffscreenCanvas(Math.ceil(ancho * dpr), Math.ceil(alto * dpr)); this.ctx = this.lienzo.getContext('2d'); this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); this.valida = false; } invalidar() { this.valida = false; } actualizar(dibujar) { if (this.valida) return; const { width, height } = this.lienzo; this.ctx.save(); this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.clearRect(0, 0, width, height); this.ctx.restore(); dibujar(this.ctx); this.valida = true; } pintarEn(ctx, x, y, ancho, alto) { ctx.drawImage(this.lienzo, x, y, ancho, alto); } liberar() { this.lienzo.width = this.lienzo.height = 0; }}Frente al elemento <canvas> suelto, OffscreenCanvas gana en dos cosas concretas. No crea un nodo del DOM, con lo que no hay riesgo de que algún selector lo alcance ni de que el recolector lo mantenga vivo por una referencia del documento. Y no tiene tamaño intrínseco que pueda influir en ningún layout, eliminando de raíz el bucle de realimentación que produce un canvas dentro de un contenedor flexible.
El caso de uso típico es una capa que cambia poco dentro de una escena que cambia mucho: la rejilla de un editor, el fondo de un gráfico, el terreno de un juego.
const fondo = new CapaCache(ancho, alto, dpr);function pintar() { fondo.actualizar(c => { dibujarRejilla(c, ancho, alto); dibujarEjes(c, ancho, alto); }); ctx.clearRect(0, 0, ancho, alto); fondo.pintarEn(ctx, 0, 0, ancho, alto); // una copia, en lugar de reconstruir dibujarDatos(ctx, escena);}Si la rejilla tiene doscientas líneas, la diferencia entre reconstruirla cada fotograma y copiarla es de doscientas operaciones de trazado frente a una de copia.
Generar imágenes sin mostrarlas
OffscreenCanvas es también la forma limpia de producir imágenes que nunca se enseñan: miniaturas, capturas para exportar, texturas procedurales, imágenes para compartir.
async function generarTarjeta(titulo, subtitulo) { const lienzo = new OffscreenCanvas(1200, 630); const c = lienzo.getContext('2d'); const g = c.createLinearGradient(0, 0, 1200, 630); g.addColorStop(0, '#1e1e2e'); g.addColorStop(1, '#313244'); c.fillStyle = g; c.fillRect(0, 0, 1200, 630); c.fillStyle = '#cdd6f4'; c.font = '700 72px system-ui'; c.fillText(titulo, 80, 300); c.fillStyle = '#89b4fa'; c.font = '400 36px system-ui'; c.fillText(subtitulo, 80, 370); return lienzo.convertToBlob({ type: 'image/png' });}const blob = await generarTarjeta('Canvas y SVG', 'Guía completa');const url = URL.createObjectURL(blob);// ... usar la url y despues URL.revokeObjectURL(url)Y lo mismo dentro de un worker, con lo que la generación de imágenes deja de bloquear la interfaz por completo.
Detección de soporte
OffscreenCanvas está disponible en los tres motores desde hace varias versiones, pero conviene comprobarlo y tener un camino de reserva, sobre todo si el objetivo incluye navegadores de sistemas antiguos:
function crearLienzo(ancho, alto) { if (typeof OffscreenCanvas !== 'undefined') { return new OffscreenCanvas(ancho, alto); } const c = document.createElement('canvas'); c.width = ancho; c.height = alto; return c;}Como ambos objetos son fuentes válidas para drawImage y su contexto 2D tiene la misma API, el resto del código no necesita saber cuál le tocó. La única diferencia práctica es convertToBlob frente a toBlob, que tienen firmas distintas —una devuelve promesa y la otra recibe callback— y conviene envolver:
function aBlob(lienzo, opciones) { if (lienzo.convertToBlob) return lienzo.convertToBlob(opciones); return new Promise(r => lienzo.toBlob(r, opciones?.type, opciones?.quality));}