wandres.dev
CANVAS: EL CONTEXTO 2D · Empezar a pintar

Las opciones del contexto: alpha, willReadFrequently y desynchronized

Conocer los atributos que se pasan a getContext, qué le dicen al navegador sobre cómo crear el búfer, y en qué casos concretos cambian el rendimiento de verdad.

⏱ 17 min

El segundo parámetro de getContext casi nunca aparece en los ejemplos, y sin embargo contiene las cuatro decisiones que más afectan al rendimiento de un canvas. No son opciones cosméticas: cambian el formato del búfer, deciden si vive en la GPU o en la memoria del sistema, y determinan si el navegador sincroniza el dibujo con la composición de la página. Elegirlas mal cuesta fotogramas; no elegirlas cuesta los que el valor por defecto no acierte.

🎯 Al terminar esta lección sabrás
  • Enumerar los atributos de contexto del canvas 2D y su valor por defecto.
  • Explicar qué gana un canvas opaco frente a uno con canal alfa.
  • Decidir cuándo activar willReadFrequently y qué se pierde al hacerlo.
  • Identificar los casos concretos en los que desynchronized merece la pena.

Los cuatro atributos

Se pasan como segundo argumento en la primera llamada a getContext. Después el contexto ya existe y el objeto se ignora sin avisar.

const ctx = canvas.getContext('2d', {
  alpha: true,               // por defecto true
  desynchronized: false,     // por defecto false
  willReadFrequently: false, // por defecto false
  colorSpace: 'srgb',        // 'srgb' o 'display-p3'
});

Los valores por defecto describen un canvas de propósito general: con transparencia, sincronizado con la composición de la página, optimizado para dibujar y no para leer, y en sRGB. Para bastantes aplicaciones esos valores no son los mejores.

Si necesitas comprobar qué te dieron de verdad, el contexto lo expone:

console.log(ctx.getContextAttributes());
// { alpha: true, colorSpace: 'srgb', desynchronized: false, willReadFrequently: false }

alpha: el canvas opaco

alpha: false le dice al navegador que el canvas no tiene transparencia. El fondo se comporta como negro opaco y todo lo que dibujes se compone sobre él.

Lo que se gana es concreto. El compositor sabe que no hay nada detrás que mostrar, así que puede saltarse el mezclado con lo que hay debajo en la página. En dispositivos con GPU integrada y en móviles eso se nota, especialmente en canvas grandes que ocupan la pantalla entera. Además, ciertos caminos de código internos se simplifican.

Lo que se pierde es la transparencia, con dos consecuencias que sorprenden:

const ctx = canvas.getContext('2d', { alpha: false });
ctx.clearRect(0, 0, 400, 300);
// El canvas queda NEGRO, no transparente. clearRect pinta negro opaco.

Y la segunda: dibujar con un color semitransparente sigue funcionando —se mezcla con lo que ya hay— pero el resultado nunca tiene alfa menor que 1. No puedes hacer que se vea el fondo de la página a través del canvas.

El caso ideal de alpha: false es un canvas que ocupa toda su caja y siempre pinta un fondo: juegos, visualizaciones a pantalla completa, vídeo procesado. El caso en el que no debes usarlo es cualquier canvas que se superponga a otro contenido, que es exactamente lo que hacen las capas de un diseño híbrido.

// Con alpha:false conviene pintar el fondo explicitamente en lugar de clearRect
function limpiar() {
  ctx.fillStyle = '#1e1e2e';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
}

willReadFrequently: cambiar GPU por CPU

Este atributo es el más malentendido de los cuatro porque su nombre describe el síntoma y no el mecanismo.

Un canvas acelerado guarda sus píxeles en memoria de vídeo. Dibujar es barato porque los comandos se encolan y la GPU los ejecuta. Leer es carísimo porque hay que vaciar la cola, esperar a que la GPU termine y copiar la memoria de vuelta al sistema. Ese viaje de vuelta bloquea el hilo y puede costar varios milisegundos por llamada.

willReadFrequently: true le dice al navegador: voy a leer mucho, así que crea este canvas en memoria del sistema desde el principio. Con eso getImageData se convierte en una copia de memoria normal y barata.

El precio es que pierdes la aceleración por hardware para todo lo demás. Los rellenos, los trazados, las rutas complejas y sobre todo drawImage con escalado pasan a ejecutarse en la CPU. Para un canvas que dibuja poco y lee mucho es una ganancia enorme; para uno que dibuja mucho es un desastre.

// Correcto: canvas de analisis, dibuja un fotograma y lo lee entero
const analisis = document.createElement('canvas');
analisis.width = 320; analisis.height = 240;
const actx = analisis.getContext('2d', { willReadFrequently: true });

function medirBrillo(video) {
  actx.drawImage(video, 0, 0, 320, 240);
  const d = actx.getImageData(0, 0, 320, 240).data;
  let suma = 0;
  for (let i = 0; i < d.length; i += 4) {
    suma += 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
  }
  return suma / (d.length / 4);
}

La estrategia que casi siempre gana es separar los canvas por rol: uno visible y acelerado para dibujar, y otro invisible y pequeño con willReadFrequently para leer. No es un truco, es la arquitectura correcta, porque además el canvas de análisis puede ser mucho más pequeño que el visible.

Los navegadores ya te avisan, y ese aviso vale más que el atributo

Chrome imprime en consola un mensaje muy concreto cuando detecta llamadas frecuentes a getImageData sobre un canvas acelerado: sugiere activar willReadFrequently. Es de los pocos avisos de rendimiento que el navegador da por iniciativa propia, y hay que hacerle caso, pero no de la forma obvia. Antes de activar el atributo, pregúntate si de verdad necesitas leer. Una enorme proporción de los usos de getImageData en producción son innecesarios: leer un píxel para saber de qué color es una zona que tú mismo pintaste, comprobar si un punto está dentro de una forma —para eso existe isPointInPath—, o detectar colisiones que se resuelven mucho mejor con geometría. Cada una de esas lecturas te está costando una sincronización completa con la GPU. Y hay una trampa adicional: la heurística del navegador puede degradar un canvas a software aunque no pongas el atributo, si detecta suficientes lecturas. Es decir, tu canvas puede haber perdido la aceleración sin que tú lo hayas pedido, y el síntoma es una caída de rendimiento general que no se explica por nada del código. Cuando un canvas que iba bien empieza a ir mal después de añadir una funcionalidad, busca la lectura de píxeles que añadiste con ella.

desynchronized: latencia por tearing

desynchronized: true pide que el canvas se salte la sincronización con el resto de la composición de la página. En lugar de esperar a que el compositor prepare el fotograma completo, el navegador puede mandar el contenido del canvas a la pantalla por un camino más corto.

Lo que se gana es latencia. En un canvas de dibujo a mano alzada, la diferencia entre que el trazo aparezca en el fotograma actual o en el siguiente es la diferencia entre que la aplicación se sienta viva o pastosa. Con lápiz digital, donde el usuario compara la punta física con la tinta en pantalla, esa diferencia es muy perceptible.

Lo que se pierde es la garantía de que el canvas y el resto de la página estén sincronizados. Puede haber tearing: ver medio fotograma nuevo y medio viejo. Y en algunos casos el navegador ignora el atributo por completo, porque depende de que la plataforma tenga un camino de baja latencia disponible.

El caso de uso es estrecho y claro: aplicaciones de tinta digital y dibujo a mano alzada. Fuera de ahí no aporta y puede introducir artefactos. La combinación que suele ir junta es desynchronized: true con alpha: false, porque un canvas opaco tiene más posibilidades de conseguir el camino directo.

const ctx = tinta.getContext('2d', { desynchronized: true, alpha: false });
tinta.addEventListener('pointermove', e => {
  // getCoalescedEvents recupera las muestras intermedias que el navegador
  // agrupo, esencial para trazos suaves con lapiz de alta frecuencia
  for (const p of e.getCoalescedEvents()) {
    ctx.lineTo(p.offsetX, p.offsetY);
  }
  ctx.stroke();
});

colorSpace y una tabla de decisión

colorSpace acepta "srgb" y "display-p3". Con P3 el búfer puede representar colores fuera del gamut de sRGB, lo que importa en pantallas de gamut amplio y en aplicaciones de imagen. Los colores que pintes se convierten al espacio del búfer, así que pedir P3 y escribir colores en notación hexadecimal no aporta nada: hay que escribirlos con la sintaxis color(display-p3 r g b).

Caso alpha willReadFrequently desynchronized
Canvas de interfaz sobre otro contenido true false false
Juego o visualización a pantalla completa false false false
Canvas de análisis de imagen o vídeo true true false
Aplicación de dibujo con lápiz false false true
Canvas de caché fuera de pantalla true false false

La fila del canvas de caché merece un comentario: un canvas auxiliar que solo se dibuja y se copia con drawImage debe mantener alpha: true casi siempre, porque su gracia es precisamente componer sobre lo que ya hay. Ponerle alpha: false lo convierte en un rectángulo negro con tu dibujo dentro.