Introducción
El lienzo HTML5, que comenzó como un experimento de Apple, es el estándar más compatible con los gráficos en modo inmediato 2D en la Web. Muchos desarrolladores ahora lo usan para una amplia variedad de proyectos, visualizaciones y juegos multimedia. Sin embargo, a medida que las aplicaciones que compilamos aumentan en complejidad, los desarrolladores se topan, de forma inadvertida, con el límite de rendimiento. Hay mucha información descontextualizada sobre la optimización del rendimiento del lienzo. El objetivo de este artículo es consolidar parte de este cuerpo en un recurso más fácil de entender para los desarrolladores. En este artículo, se incluyen optimizaciones fundamentales que se aplican a todos los entornos de gráficos por computadora, así como técnicas específicas del lienzo que están sujetas a cambios a medida que mejoran las implementaciones del lienzo. En particular, a medida que los proveedores de navegadores implementen la aceleración de GPU del lienzo, es probable que algunas de las técnicas de rendimiento descritas sean menos impactantes. Esto se hará constar cuando corresponda. Ten en cuenta que este artículo no explica el uso del lienzo HTML5. Para ello, consulta estos artículos relacionados con el lienzo en HTML5Rocks, este capítulo en el sitio Dive into HTML5 o el instructivo Canvas de MDN.
Pruebas de rendimiento
Para abordar el mundo cambiante del lienzo HTML5, las pruebas de JSPerf (jsperf.com) verifican que cada optimización propuesta siga funcionando. JSPerf es una aplicación web que permite a los desarrolladores escribir pruebas de rendimiento de JavaScript. Cada prueba se enfoca en un resultado que intentas lograr (por ejemplo, borrar el lienzo) y, además, incluye varios enfoques que logran el mismo resultado. JSPerf ejecuta cada enfoque tantas veces como sea posible en un período breve y proporciona una cantidad de iteraciones por segundo significativa desde el punto de vista estadístico. Las puntuaciones más altas siempre son mejores. Los visitantes de una página de prueba de rendimiento de JSPerf pueden ejecutar la prueba en su navegador y permitir que JSPerf almacene los resultados de la prueba normalizados en Browserscope (browserscope.org). Dado que las técnicas de optimización de este artículo tienen una copia de seguridad de un resultado de JSPerf, puedes volver para ver información actualizada sobre si la técnica aún se aplica o no. Escribí una pequeña aplicación de ayuda que renderiza estos resultados como gráficos, incorporados en todo este artículo.
Todos los resultados de rendimiento de este artículo se basan en la versión del navegador. Esto resulta ser una limitación, ya que no sabemos en qué SO se ejecutaba el navegador o, lo que es más importante, si el lienzo HTML5 se aceleró con hardware cuando se ejecutó la prueba de rendimiento. Para averiguar si el lienzo HTML5 de Chrome está acelerado por hardware, visita about:gpu
en la barra de direcciones.
Renderización previa en un lienzo fuera de la pantalla
Si vuelves a dibujar primitivas similares en la pantalla en varios fotogramas, como suele ocurrir cuando se escribe un juego, puedes obtener grandes mejoras de rendimiento si renderizas previamente grandes partes de la escena. La renderización previa consiste en usar un lienzo (o lienzos) fuera de la pantalla para renderizar imágenes temporales y, luego, renderizar los lienzos fuera de la pantalla en el visible. Por ejemplo, supongamos que vuelves a dibujar a Mario corriendo a 60 fotogramas por segundo. Podrías volver a dibujar su sombrero, su bigote y su "M" en cada fotograma, o bien renderizar previamente a Mario antes de ejecutar la animación. Sin renderización previa:
// canvas, context are defined
function render() {
drawMario(context);
requestAnimationFrame(render);
}
Renderizació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 el uso de requestAnimationFrame
, que se analiza con más detalle en una sección posterior.
Esta técnica es especialmente eficaz cuando la operación de renderización (drawMario
en el ejemplo anterior) es costosa. Un buen ejemplo de esto es la renderización de texto, que es una operación muy costosa.
Sin embargo, el rendimiento deficiente del caso de prueba “prerenderizado suelto” Cuando realices el procesamiento previo, es importante asegurarte de que el lienzo temporal se ajuste firmemente alrededor de la imagen que estás dibujando. De lo contrario, la mejora en el rendimiento de la renderización fuera de la pantalla se contraponderará con la pérdida de rendimiento que se obtiene al copiar un lienzo grande en otro (lo que varía según el tamaño del objetivo de origen). Un lienzo ajustado en la prueba anterior es simplemente más pequeño:
can2.width = 100;
can2.height = 40;
En comparación con el formato flexible que genera un rendimiento más bajo:
can3.width = 300;
can3.height = 100;
Cómo agrupar llamadas a lienzo
Dado que el dibujo es una operación costosa, es más eficiente cargar la máquina de estados de dibujo con un conjunto largo de comandos y, luego, volcarlos todos en el búfer de video.
Por ejemplo, cuando dibujas varias líneas, es más eficiente crear una ruta con todas las líneas y dibujarla con una sola llamada de dibujo. En otras palabras, en lugar de dibujar líneas separadas, haz 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();
}
obtenemos un mejor rendimiento al dibujar una sola 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 mundo del lienzo HTML5. Cuando se dibuja una ruta compleja, por ejemplo, es mejor colocar todos los puntos en la ruta, en lugar de renderizar los segmentos por separado (jsperf).
Sin embargo, ten en cuenta que con Canvas hay una excepción importante a esta regla: si las primitivas involucradas en el dibujo del objeto deseado tienen cuadros de límite pequeños (por ejemplo, líneas horizontales y verticales), puede ser más eficiente renderizarlas por separado (jsperf).
Evita cambios innecesarios de estado del lienzo
El elemento lienzo HTML5 se implementa sobre una máquina de estados que realiza un seguimiento de elementos como los estilos de relleno y trazo, además de los puntos anteriores que conforman la ruta actual. Cuando se intenta optimizar el rendimiento de los gráficos, es tentador enfocarse solo en la renderización de gráficos. Sin embargo, manipular la máquina de estados también puede generar una sobrecarga de rendimiento. Por ejemplo, si usas varios colores de relleno para renderizar una escena, es más económico renderizar por color en lugar de por posición en el lienzo. Para renderizar un patrón de rayas, puedes renderizar una raya, cambiar de color, renderizar la siguiente raya, etcétera:
for (var i = 0; i < STRIPES; i++) {
context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
context.fillRect(i * GAP, 0, GAP, 480);
}
También puedes renderizar todas las rayas extrañas y, luego, todas las rayas 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);
}
Como se esperaba, el enfoque entrelazado es más lento porque cambiar la máquina de estados es costoso.
Renderiza solo las diferencias de pantalla, no todo el estado nuevo.
Como es de esperar, renderizar menos en la pantalla es más económico que renderizar más. Si solo tienes diferencias incrementales entre las recomposiciones, puedes obtener un aumento significativo del rendimiento con solo dibujar la diferencia. En otras palabras, en lugar de borrar toda la pantalla antes de dibujar, haz lo siguiente:
context.fillRect(0, 0, canvas.width, canvas.height);
Haz un seguimiento del cuadro de límite dibujado y solo bórralo.
context.fillRect(last.x, last.y, last.width, last.height);
Si conoces las gráficos por computadora, es posible que también conozcas esta técnica como “regiones de nueva creación”, en las que se guarda el cuadro de límite renderizado anteriormente y, luego, se borra en cada renderización. Esta técnica también se aplica a los contextos de renderización basados en píxeles, como se ilustra en esta conferencia sobre el emulador de Nintendo de JavaScript.
Usa lienzos de varias capas para escenas complejas
Como se mencionó antes, dibujar imágenes grandes es costoso y se debe evitar si es posible. Además de usar otro lienzo para renderizar fuera de la pantalla, como se ilustra en la sección de renderización previa, también podemos usar lienzos superpuestos. Cuando usamos transparencia en el lienzo de primer plano, podemos confiar en la GPU para combinar los alfas en el momento de la renderización. Puedes configurarlo de la siguiente manera, con dos lienzos posicionados de forma absoluta, uno sobre el otro.
<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 lienzo aquí es que, cuando dibujamos o borramos el lienzo en primer plano, nunca modificamos el fondo. Si tu juego o app multimedia se puede dividir en primer y segundo plano, procura renderizar estos elementos en lienzos separados para obtener un aumento significativo del rendimiento.
A menudo, puedes aprovechar la percepción humana imperfecta y renderizar el fondo solo una vez o a una velocidad más lenta en comparación con el primer plano (que es probable que ocupe la mayor parte de la atención del usuario). Por ejemplo, puedes renderizar el primer plano cada vez que lo hagas, pero renderizar el fondo solo cada N fotogramas. Además, ten en cuenta que este enfoque se generaliza bien para cualquier cantidad de lienzos compuestos si tu aplicación funciona mejor con este tipo de estructura.
Evita shadowBlur
Al igual que muchos otros entornos gráficos, el lienzo HTML5 permite a los desarrolladores desenfocar 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);
Conoce varias formas de despejar el lienzo
Dado que el lienzo de HTML5 es un paradigma de dibujo en modo inmediato, se debe volver a dibujar la escena de manera explícita en cada fotograma. Por este motivo, borrar el lienzo es una operación fundamental para las apps y los juegos de lienzo HTML5.
Como se menciona en la sección Cómo evitar cambios en el estado del lienzo, a menudo no es conveniente borrar todo el lienzo, pero si debes hacerlo, hay dos opciones: llamar a context.clearRect(0, 0, width, height)
o usar un hack específico del lienzo para hacerlo: canvas.width = canvas.width
. En el momento de escribir este artículo, clearRect
suele tener un mejor rendimiento que la versión de restablecimiento de ancho, pero en algunos casos, usar el hack de restablecimiento de canvas.width
es mucho más rápido en Chrome 14.
Ten cuidado con esta sugerencia, ya que depende en gran medida de la implementación subyacente del lienzo y está sujeta a cambios. Para obtener más información, consulta el artículo de Simon Sarris sobre cómo borrar el lienzo.
Evita las coordenadas de punto flotante
El lienzo HTML5 admite la renderización de subpíxeles, y no hay forma de desactivarla. Si dibujas con coordenadas que no son números enteros, se usa automáticamente el suavizado para tratar de suavizar las líneas. Este es el efecto visual, tomado de este artículo sobre el rendimiento del lienzo de subpíxeles de Seb Lee-Delisle:
Si el objeto suavizado no es el efecto que buscas, puede ser mucho más rápido convertir tus coordenadas en números enteros con Math.floor
o Math.round
(jsperf):
Para convertir tus coordenadas de punto flotante en números enteros, puedes usar varios métodos inteligentes, entre los que la más eficiente consiste en agregar una mitad al número objetivo y, luego, realizar operaciones de bits en el resultado para eliminar la parte fraccionaria.
// 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;
Puedes consultar el desglose completo del rendimiento aquí (jsperf).
Ten en cuenta que este tipo de optimización ya no debería importar una vez que las implementaciones de lienzo estén aceleradas por GPU, lo que permitirá renderizar rápidamente coordenadas no enteras.
Optimiza tus animaciones con requestAnimationFrame
La API de requestAnimationFrame
relativamente nueva es la forma recomendada de implementar aplicaciones interactivas en el navegador. En lugar de pedirle al navegador que renderice a una tasa de marca fija determinada, solicítale cortésmente que llame a tu rutina de renderización y que lo llame cuando el navegador esté disponible. Como un buen efecto secundario, si la página no está en primer plano, el navegador es lo suficientemente inteligente para no renderizarla.
La devolución de llamada requestAnimationFrame
busca una tasa de devolución de llamada de 60 FPS, pero no lo garantiza, por lo que debes realizar un seguimiento del tiempo que transcurrió desde la última renderización. Esto puede verse de la siguiente manera:
var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
var delta = Date.now() - 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 lienzo y a otras tecnologías de renderización, como WebGL.
Al momento de la redacción, esta API solo está disponible en Chrome, Safari y Firefox, por lo que debes usar esta corrección de compatibilidad.
La mayoría de las implementaciones de lienzo para dispositivos móviles son lentas.
Hablemos de los dispositivos móviles. Lamentablemente, en el momento de escribir este artículo, solo la versión beta de iOS 5.0 que ejecuta Safari 5.1 tiene la implementación de lienzo para dispositivos móviles con aceleración de GPU. Sin la aceleración de la GPU, los navegadores para dispositivos móviles, por lo general, no tienen CPUs lo suficientemente potentes para las aplicaciones modernas basadas en lienzo. Algunas de las pruebas de JSPerf que se describieron anteriormente tienen un orden de magnitud peor en dispositivos móviles en comparación con computadoras de escritorio, lo que restringe en gran medida los tipos de apps multidispositivo que puedes ejecutar de forma correcta.
Conclusión
En resumen, este artículo cubrió un conjunto completo de técnicas de optimización útiles que te ayudarán a desarrollar proyectos de lienzo en formato HTML5 de buen rendimiento. Ahora que aprendiste algo nuevo aquí, ve a optimizar tus increíbles creaciones. O bien, si actualmente no tienes un juego o una aplicación para optimizar, consulta Chrome Experiments y Creative JS para inspirarte.
Referencias
- Modo inmediato frente a modo retenido
- Otros artículos sobre Canvas de HTML5Rocks.
- La sección Canvas de Dive into HTML5.
- JSPerf permite a los desarrolladores crear pruebas de rendimiento de JS.
- Browserscope almacena datos de rendimiento del navegador.
- JSPerfView, que renderiza las pruebas de JSPerf como gráficos
- La entrada de blog de Simon sobre cómo borrar el lienzo y su libro, HTML5 Unleashed, que incluye capítulos sobre el rendimiento de Canvas
- Entrada de blog de Sebastian sobre el rendimiento de la renderización de subpíxeles.
- Charla de Ben sobre la optimización de un emulador de NES de JS.
- El nuevo generador de perfiles de lienzo en las Herramientas para desarrolladores de Chrome.