Mejora del rendimiento del elemento canvas de HTML5

HTML5 Rocks

Introducción

El elemento canvas de HTML5, que comenzó como un experimento de Apple, es el estándar más compatible para gráficos de modo inmediato en 2D en la Web. Actualmente, muchos desarrolladores lo utilizan para gran variedad de juegos, visualizaciones y proyectos multimedia. Sin embargo, a medida que las aplicaciones que creamos aumentan en complejidad, los desarrolladores superan la barrera del rendimiento de forma inadvertida.

Existen muchos conocimientos inconexos sobre cómo optimizar el rendimiento del elemento canvas. Con este artículo se pretende consolidar parte de este conjunto de conocimientos y convertirlo en un recurso más fácil de asimilar para los desarrolladores. En este artículo, se incluyen optimizaciones fundamentales que se aplican a todos los entornos relacionados con los gráficos por ordenador, así como técnicas específicas del elemento canvas que están sujetas a cambios a medida que mejoran las implementaciones de este elemento. En particular, a medida que los desarrolladores de navegadores implementan la aceleración por GPU del elemento canvas, aumentan las probabilidades de que algunas de las técnicas de rendimiento mencionadas tengan menos impacto. Este tema se tratará cuando corresponda.

Ten en cuenta que este artículo no trata sobre el uso del elemento canvas de HTML5. Para ello, consulta estos artículos relacionados con el elemento canvas en HTML5Rocks, este capítulo de Dive into HTML5 y el Tutorial de Canvas de Mozilla Developer Network.

Pruebas de rendimiento

Para abordar los rápidos cambios que afectan al entorno del elemento canvas de HTML5, las pruebas de JSPerf (jsperf.com) verifican que cada una de las propuestas de optimización sigan funcionando. JSPerf es una aplicación web que permite a los desarrolladores crear pruebas de rendimiento mediante JavaScript. Cada prueba se centra en un resultado que se intenta conseguir (por ejemplo, borrar el elemento canvas) e incluye varios métodos con los que se obtiene el mismo resultado. JSPerf ejecuta cada método tantas veces como sea posible durante un breve período de tiempo y proporciona un número de iteraciones por segundo que resulta significativo desde el punto de vista estadístico. Las puntuaciones más altas siempre son las mejores.

Los usuarios de la página de pruebas de rendimiento de JSPerf pueden realizar la prueba en su propio navegador y obtienen los resultados normalizados de la prueba en Browserscope (browserscope.org). Debido a que las técnicas de optimización de este artículo están respaldadas por un resultado de JSPerf, puedes volver para obtener información actualizada sobre si la técnica se sigue aplicando. He creado una pequeña aplicación de ayuda que muestra estos resultados en forma de gráficos, que se han incluido a lo largo de este artículo.

Todos los resultados de rendimiento de este artículo dependen de la versión del navegador. Esto acaba siendo una limitación, ya que no se sabe en qué sistema operativo se estaba ejecutando el navegador, o lo que es más importante, si el elemento canvas de HTML5 se había acelerado por hardware cuando se realizó la prueba de rendimiento. Para averiguar si el elemento canvas de HTML5 de Chrome se ha acelerado por hardware, introduce about:gpu en la barra de direcciones.

Realiza una representación previa en un elemento canvas fuera de la pantalla

Si estás dibujando de nuevo primitivas similares en la pantalla utilizando varios fotogramas, como sucede a menudo cuando se crea un juego, puedes obtener grandes beneficios de rendimiento al representar previamente partes de gran tamaño de la escena. Realizar una representación previa significa utilizar un elemento canvas (o varios elementos canvas) fuera de la pantalla independiente para representar imágenes temporales en él y luego volver a representar los elementos canvas fuera de la pantalla en el elemento canvas visible. Para los desarrolladores que estén familiarizados con los gráficos por ordenador, esta técnica también se conoce como lista de despliegue.

Por ejemplo, supongamos que estás redibujando a Mario ejecutándose a 60 fotogramas por segundo. Puedes redibujar su gorra, su bigote y la "M" en cada fotograma o representar previamente a Mario antes de ejecutar la animación.

Sin representación previa:

// canvas, context are defined
function render() {
  drawMario(context);
  requestAnimationFrame(render);
}

Con representación previa:

var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext(‘2d’);
drawMario(m_context);

function render() {
  context.drawImage(m_canvas, 0, 0);
  requestAnimationFrame(render);
}

Ten en cuenta que debes utilizar requestAnimationFrame, que se explica más detalladamente en una sección posterior. El siguiente gráfico ilustra las ventajas de rendimiento que se obtienen al utilizar la representación previa (de esta prueba de jsperf):

Esta técnica es especialmente efectiva cuando la operación de representación (drawMario en el ejemplo anterior) supone un desembolso económico elevado. Un buen ejemplo de esto es la representación de texto, ya que se trata de una operación muy costosa. A continuación, se muestra el drástico aumento del rendimiento que puedes esperar si representas previamente esta operación (procedente de esta prueba de jsperf):

Sin embargo, en el ejemplo anterior se observa el bajo rendimiento del caso de prueba con "holgura en la representación previa". Al realizar la representación previa, es importante que te asegures de que el elemento canvas se ajuste perfectamente alrededor de la imagen que estés dibujando; de lo contrario, la mejora de rendimiento de la representación fuera de la pantalla se anulará por la pérdida de rendimiento que supone copiar un elemento canvas de gran tamaño en otro (que varía en función del tamaño de origen y de destino). Los canvas ajustados de la prueba anterior son simplemente más pequeños.

can2.width = 100;
can2.height = 40;

En comparación con el canvas holgado que obtiene un rendimiento más bajo.

can3.width = 300;
can3.height = 100;

Agrupa las llamadas del elemento canvas

Debido a que el proceso de dibujo es una operación costosa, es más eficiente cargar la máquina de estado de dibujo con un gran conjunto de comandos y, a continuación, volcarlos todos en el búfer de vídeo.

Por ejemplo, al dibujar varias líneas, resulta más eficiente crear un trazado que contenga todas las líneas y dibujarlo con una única llamada de dibujo. En otras palabras, en lugar de dibujar líneas por separado, realiza lo siguiente:

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}

Se obtiene mayor rendimiento si se dibuja una única polilínea:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

Esto también se aplica al entorno del elemento canvas de HTML5. Al dibujar un trazado complejo, por ejemplo, es más adecuado colocar todos los puntos dentro del trazado en lugar de representar los segmentos por separado (jsperf).

Ten en cuenta, sin embargo, que existe una excepción importante a esta regla en lo que respecta al elemento canvas: si las primitivas utilizadas para dibujar el objeto deseado tienen cuadros delimitadores pequeños (por ejemplo, líneas verticales y horizontales), es posible que sea más eficiente representarlos por separado (jsperf).

Evita cambios de estado innecesarios del elemento canvas

El elemento canvas de HTML5 se implementa en una máquina de estado que controla aspectos como los estilos de relleno y de trazo, así como puntos anteriores que forman el trazado actual. Al intentar optimizar el rendimiento de los gráficos, se intenta centrar únicamente en los gráficos que está representando. Sin embargo, si se manipula la máquina de estado también se puede producir una sobrecarga de rendimiento.

Por ejemplo, si utilizas varios colores de relleno para representar una escena, es menos costoso representar cada uno de los colores que colocarlos en el elemento canvas. Para representar un patrón de raya diplomática, puedes representar una raya, cambiar los colores, representar la siguiente raya, etc.:

for (var i = 0; i < STRIPES; i++) {
  context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
  context.fillRect(i * GAP, 0, GAP, 480);
}

O representar todas las rayas impares y luego todas las pares:

context.fillStyle = COLOR1;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2) * GAP, 0, GAP, 480);
}
context.fillStyle = COLOR2;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}

En la siguiente prueba de rendimiento, se dibuja un patrón de raya diplomática intercalado utilizando los dos métodos (jsperf).

Tal y como se esperaba, el método intercalado es más lento debido a que al cambio de la máquina de estado resulta costoso.

Representa únicamente las diferencias de pantalla, no todo el estado nuevo

Como sería de esperar, representar una cantidad reducida de elementos en la pantalla es más barato que representar una cantidad mayor. Si solo dispones de diferencias incrementales entre redibujos, puedes obtener un aumento significativo del rendimiento simplemente dibujando la diferencia. En otras palabras, en lugar de borrar toda la pantalla antes de dibujar, realiza lo siguiente:

context.fillRect(0, 0, canvas.width, canvas.height);

Controla el cuadro delimitador dibujado y borra únicamente eso.

context.fillRect(last.x, last.y, last.width, last.height);

Esto se ilustra en la prueba de rendimiento que se muestra a continuación, en la que un punto blanco cruza la pantalla (jsperf):

Si estás familiarizado con los gráficos por ordenador, es posible que también conozcas esta técnica como "regiones de redibujo", mediante la cual se guarda el cuadro delimitador representado previamente y, a continuación, se borra en cada representación.

Esta técnica también se aplica a los contextos de representación basados en píxeles, tal y como se ilustra en esta charla sobre el emulador de Nintendo en JavaScript.

Utiliza elementos canvas de varias capas para escenas complejas

Como ya se ha mencionado, dibujar imágenes grandes es costoso y se debe evitar en la medida de lo posible. Además de utilizar otro canvas para la representación fuera de la pantalla, tal y como se ha ilustrado en la sección sobre la representación previa, también podemos utilizar elementos canvas con capas superpuestos. Si el elemento canvas de primer plano es transparente, podremos confiar en que la GPU componga las capas alfa en el momento de la representación. Puedes configurarlo como se indica a continuación, utilizando dos canvas perfectamente superpuestos.

<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>

La ventaja de tener solo un elemento canvas es que al dibujar o borrar el elemento canvas de primer plano, nunca se modifica el fondo. Si tu juego o aplicación multimedia se puede dividir en primer plano y fondo, considera representarlos en elementos canvas independientes para conseguir un aumento significativo del rendimiento. En el siguiente gráfico se compara el caso del elemento canvas simple con otro caso en el que únicamente se redibuja y se borra el primer plano (jsperf):

A menudo, podrás aprovechar la ventaja de que la percepción humana es imperfecta para representar el fondo una sola vez o a una velocidad más lenta en comparación con el primer plano (que probablemente requerirá la mayor parte de la atención del usuario). Por ejemplo, puedes representar el primer plano cada vez que se realice una representación, pero representar el fondo solo cada cierto número de fotogramas.

También debes tener en cuenta que, en general, este método se puede aplicar a cualquier número de elementos canvas compuestos si tu aplicación funciona mejor con este tipo de estructura.

Evita shadowBlur

Al igual que otros entornos gráficos, el elemento canvas de HTML5 permite a los desarrolladores desenfocar las primitivas, pero esta operación puede ser muy costosa.

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);

La siguiente prueba de rendimiento muestra la misma escena representada con sombra y sin sombra y la enorme diferencia entre ambas representaciones (jsperf):

Descubre distintas formas de borrar el elemento canvas

Puesto que el elemento canvas de HTML5 es un paradigma de dibujo de modo inmediato, es necesario redibujar la escena de forma explícita en cada fotograma. Por esta razón, borrar el elemento canvas es una operación extremadamente importante para aplicaciones y juegos elaborados mediante el elemento canvas de HTML5.

Tal y como se ha mencionado en la sección Evita cambios de estado innecesarios del elemento canvas, normalmente no es recomendable borrar todo el elemento canvas, pero si debes hacerlo, dispones de dos opciones: ejecutar context.clearRect(0, 0, width, height) o utilizar un código de pirateo específico de canvas (canvas.width = canvas.width) para hacerlo.

En lo que respecta a la escritura de código, clearRect suele ofrecer mejores resultados que la versión de restablecimiento de ancho, pero en algunos casos, el uso del código de pirateo de restablecimiento canvas.width es significativamente más rápido en Chrome 14 (jsperf).

Ten cuidado con esta sugerencia, ya que depende en gran medida de la implementación del elemento canvas subyacente y está muy sujeta a cambios. Para obtener más información, consulta el artículo de Simon Sarris sobre cómo borrar un elemento canvas.

Evita coordenadas de puntos flotantes

El elemento canvas de HTML5 admite la representación por subpíxeles y no es posible desactivarla. Si dibujas con coordenadas que no son números enteros, este elemento utiliza de forma automática antialiasing para tratar de suavizar las líneas. A continuación se muestra el efecto visual, tomado de este artículo sobre el rendimiento del elemento canvas con subpíxeles publicado por Seb Lee-Delisle.

subpíxeles

Si la atenuación del sprite no es el efecto que buscas, puede ser más rápido convertir tus coordenadas en números enteros mediante Math.floor o Math.round (jsperf).

Para convertir las coordenadas de puntos flotantes en números enteros, puedes utilizar varias técnicas ingeniosas, la más eficiente de ellas implica añadir un medio al número de destino y, a continuación realizar operaciones a nivel de bit en el resultado para eliminar la parte fraccional.

// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;

A continuación, se muestra el desglose de rendimiento completo (jsperf):

Ten en cuenta que este tipo de optimización ya no debe tener importancia una vez se aceleren por GPU las implementaciones del elemento canvas, ya que se podrán representar de forma rápida coordenadas que no sean números enteros.

Optimiza tus animaciones con "requestAnimationFrame"

El API relativamente nueva requestAnimationFrame es el método que se recomienda para implementar aplicaciones interactivas en el navegador. En lugar de indicar al navegador que realice una representación a una velocidad de marcado fija, puedes pedirle educadamente que ejecute tu rutina de representación y que reciba una llamada cuando se encuentre disponible. Como consecuencia, si la página no se encuentra en primer plano, el navegador es lo suficientemente listo como para no representarla.

La devolución de llamada de requestAnimationFrame se debe ejecutar a una velocidad de 60 fotogramas por segundo, pero eso no significa que siempre sea así, por lo que tendrás que controlar el tiempo transcurrido desde la última representación. Sería algo como esto:

var x = 100;
var y = 100;
var lastRender = new Date();
function render() {
  var delta = new Date() - lastRender;
  x += delta;
  y += delta;
  context.fillRect(x, y, W, H);
  requestAnimationFrame(render);
}
render();

Ten en cuenta que este uso de requestAnimationFrame se aplica al elemento canvas y a otras tecnologías de representación como WebGL.

En lo que respecta a la escritura de código, esta API solo se encuentra disponible en Chrome, Safari y Firefox, por lo que debes utilizar estas correcciones de compatibilidad.

La mayoría de las implementaciones del elemento canvas son lentas

Hablemos de dispositivos móviles. Desafortunadamente, en lo que respecta a la escritura de código, solo el sistema operativo iOS 5.0 beta con Safari 5.1 cuenta con implementación de elementos canvas acelerada por GPU para dispositivos móviles. Sin aceleración por GPU, los navegadores para dispositivos móviles suelen disponer de CPU lo suficientemente potentes como para admitir aplicaciones modernas basadas en elementos canvas. Varias de las pruebas de JSPerf descritas anteriormente ejecutan una orden de menor magnitud en dispositivos móviles que en ordenadores, lo que reduce significativamente los tipos de aplicaciones de diferentes dispositivos que se espera que funcionen correctamente.

Conclusión

En resumen, en este artículo se ha tratado un completo conjunto de técnicas de optimización útiles que te ayudarán a desarrollar proyectos eficientes basados en elementos canvas de HTML5. Ahora que has aprendido algo nuevo, ve más allá y optimiza tus increíbles creaciones. O si todavía no tienes un juego o una aplicación que optimizar, puedes obtener algunas ideas en Chrome Experiments y en Creative JS.

Referencias

Comments

0