createImageBitmap y sus opciones
Usar el objeto de imagen optimizado del canvas: recorte, reescalado, orientación, alfa y espacio de color en la propia decodificación, y liberación explícita.
Qué es y por qué existe
const bitmap = await createImageBitmap(fuente, opciones);Un ImageBitmap es una imagen ya decodificada, sin ningún estado de carga y sin ninguna relación con el DOM. Frente a una HTMLImageElement, gana en tres cosas.
No hay decodificación en el momento del dibujo. Una <img> puede llegar al drawImage con los datos comprimidos aún sin descomprimir, y entonces el navegador decodifica en ese instante, en el hilo principal, con un tirón proporcional al tamaño. Un ImageBitmap ya está listo.
Se puede transferir a un worker. Es un objeto transferible: postMessage puede moverlo sin copiar los píxeles.
Se puede liberar. Tiene un método close() que suelta la memoria de inmediato, sin esperar al recolector.
Las fuentes admitidas son las mismas de drawImage más dos importantes: un Blob —lo que devuelve fetch con datos de imagen— y un ImageData.
const respuesta = await fetch('/foto.jpg');const blob = await respuesta.blob();const bitmap = await createImageBitmap(blob);ctx.drawImage(bitmap, 0, 0);Ese camino evita por completo el elemento <img> y su ciclo de carga, y la decodificación ocurre fuera del hilo principal en los navegadores actuales.
Recorte en la creación
Hay una variante con cuatro parámetros más que recorta durante la decodificación:
const bitmap = await createImageBitmap(fuente, sx, sy, sAncho, sAlto, opciones);La ventaja frente a recortar con drawImage es que el bitmap resultante solo contiene la región recortada, así que ocupa menos memoria. En un atlas grande del que solo necesitas unas celdas, la diferencia es real.
Una peculiaridad útil: el rectángulo de recorte puede salirse de la imagen, y las zonas que quedan fuera se rellenan con transparente. Eso permite crear un bitmap con margen alrededor de un sprite sin manipular la imagen original.
Las opciones
const bitmap = await createImageBitmap(fuente, { imageOrientation: 'from-image', // 'from-image' | 'flipY' | 'none' premultiplyAlpha: 'default', // 'none' | 'premultiply' | 'default' colorSpaceConversion: 'default', // 'none' | 'default' resizeWidth: 256, resizeHeight: 256, resizeQuality: 'high', // 'pixelated' | 'low' | 'medium' | 'high'});imageOrientation decide qué hacer con los metadatos de orientación de un JPEG. Con 'from-image' se respeta la orientación indicada en los metadatos, que es lo que quieres para fotos hechas con móvil, donde la imagen está girada y solo un dato de la cabecera dice cómo. Con 'flipY' se voltea verticalmente, que es lo que necesitan las texturas de WebGL. Con 'none' se ignora la orientación y se toman los píxeles tal cual.
premultiplyAlpha controla si los canales de color vienen ya multiplicados por el alfa. El valor por defecto deja decidir al navegador, que elige lo más eficiente. Pedir 'none' es lo correcto cuando vas a leer los píxeles y necesitas los valores de color reales.
colorSpaceConversion con 'none' desactiva la conversión al espacio de color del contexto, lo que importa cuando procesas la imagen numéricamente y quieres los valores originales del fichero.
resizeWidth, resizeHeight y resizeQuality reescalan durante la decodificación. Esta es la opción de mayor impacto práctico y la que menos se usa.
Reescalar al decodificar
Cargar una foto de doce megapíxeles para mostrarla como una miniatura de 200 píxeles es un desperdicio de 48 MB de memoria y de un tirón de decodificación completo. Con resizeWidth el navegador puede decodificar directamente a la escala pedida, lo que en varios formatos es mucho más barato que decodificar entero y reducir después.
async function miniatura(blob, lado = 200) { return createImageBitmap(blob, { resizeWidth: lado, resizeHeight: lado, resizeQuality: 'high', });}Ojo: resizeWidth y resizeHeight no conservan la proporción. Si solo especificas uno, el otro se calcula manteniendo la relación de aspecto; si especificas los dos, la imagen se deforma. Para un ajuste tipo cover hay que calcular las dimensiones antes:
async function miniaturaProporcional(blob, ladoMax) { const previo = await createImageBitmap(blob); const escala = ladoMax / Math.max(previo.width, previo.height); const w = Math.round(previo.width * escala); const h = Math.round(previo.height * escala); previo.close(); // liberar el grande cuanto antes return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h, resizeQuality: 'high' });}Esa doble decodificación parece derrochadora y sigue siendo mucho mejor que mantener la imagen completa en memoria, sobre todo si el Blob viene de un fichero que el usuario acaba de seleccionar y hay decenas de ellos.
resizeQuality: 'pixelated' usa vecino más próximo, que es lo que quieres para escalar pixel art sin filtrado.
Un cargador con caché y límite
Juntando todo, este es el patrón que usa una aplicación seria:
class CacheDeBitmaps { constructor(maximo = 60) { this.max = maximo; this.mapa = new Map(); } async obtener(url, opciones) { const clave = url + JSON.stringify(opciones ?? {}); const existente = this.mapa.get(clave); if (existente) { this.mapa.delete(clave); // reinsertar = mas reciente this.mapa.set(clave, existente); return existente; } const promesa = fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.blob(); }) .then(b => createImageBitmap(b, opciones)); this.mapa.set(clave, promesa); this.podar(); return promesa; } podar() { while (this.mapa.size > this.max) { const vieja = this.mapa.keys().next().value; const p = this.mapa.get(vieja); this.mapa.delete(vieja); Promise.resolve(p).then(b => b.close?.()).catch(() => {}); } } vaciar() { for (const p of this.mapa.values()) { Promise.resolve(p).then(b => b.close?.()).catch(() => {}); } this.mapa.clear(); }}Tres detalles. Se guarda la promesa, no el bitmap, para que dos peticiones simultáneas de la misma imagen no decodifiquen dos veces. El Map reinsertado implementa expulsión por uso menos reciente sin estructura adicional. Y la poda cierra los bitmaps expulsados, que es el punto entero de tener una caché con techo en lugar de dejar que crezca.