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.
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 clearRectfunction 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 enteroconst 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.
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.