Introducción
Quieres que tu app web se sienta responsiva y fluida cuando realices animaciones, transiciones y otros efectos pequeños de la IU. Asegurarse de que estos efectos no tengan tirones puede marcar la diferencia entre una sensación "nativa" o una tosca y poco pulida.
Este es el primero de una serie de artículos que abordan la optimización del rendimiento de renderización en el navegador. Para comenzar, veremos por qué es difícil lograr una animación fluida y qué se debe hacer para lograrlo, además de algunas prácticas recomendadas sencillas. Muchas de estas ideas se presentaron originalmente en "Jank Busters", una charla que Nat Duca y yo dimos en Google I/O (video) este año.
Presentación de V-sync
Es posible que los jugadores de PC conozcan este término, pero no es común en la Web: ¿qué es la sincronización vertical?
Considera la pantalla de tu teléfono: se actualiza en un intervalo regular, por lo general (pero no siempre), unas 60 veces por segundo. La sincronización vertical (o V-sync) se refiere a la práctica de generar fotogramas nuevos solo entre las actualizaciones de pantalla. Puedes pensar en esto como una condición de carrera entre el proceso que escribe datos en el búfer de pantalla y el sistema operativo que lee esos datos para mostrarlos en la pantalla. Queremos que el contenido de los fotogramas almacenados en el búfer cambie entre estas actualizaciones, no durante ellas; de lo contrario, el monitor mostrará la mitad de un fotograma y la mitad de otro, lo que provocará un "desgarro".
Para obtener una animación fluida, necesitas que haya un fotograma nuevo listo cada vez que se actualice la pantalla. Esto tiene dos implicaciones importantes: la sincronización de fotogramas (es decir, cuándo debe estar listo el fotograma) y el presupuesto de fotogramas (es decir, cuánto tiempo tiene el navegador para producir un fotograma). Solo tienes el tiempo entre las actualizaciones de pantalla para completar un fotograma (aproximadamente 16 ms en una pantalla de 60 Hz) y quieres comenzar a producir el siguiente fotograma en cuanto se muestre el último en la pantalla.
El momento adecuado es lo más importante: requestAnimationFrame
Muchos desarrolladores web usan setInterval o setTimeout cada 16 milisegundos para crear animaciones. Esto es un problema por varios motivos (y hablaremos más al respecto en un minuto), pero los más preocupantes son los siguientes:
- La resolución del temporizador de JavaScript es de solo unos milisegundos
- Diferentes dispositivos tienen diferentes frecuencias de actualización
Recuerda el problema de latencia de fotogramas que se mencionó anteriormente: necesitas un fotograma de animación completo, que haya terminado con cualquier JavaScript, manipulación del DOM, diseño, pintura, etc., para que esté listo antes de que se produzca la próxima actualización de la pantalla. La baja resolución del temporizador puede dificultar que se completen los fotogramas de animación antes de la siguiente actualización de la pantalla, pero la variación en las frecuencias de actualización de la pantalla lo hace imposible con un temporizador fijo. Sin importar cuál sea el intervalo del temporizador, te saldrás lentamente de la ventana de tiempo de un fotograma y terminarás perdiendo uno. Esto sucedería incluso si el temporizador se activara con una precisión de milisegundos, lo que no ocurrirá (como descubrieron los desarrolladores). La resolución del temporizador varía según si la máquina está conectada a la batería o enchufada, puede verse afectada por las pestañas en segundo plano que acaparan recursos, etc. Incluso si esto es poco frecuente (por ejemplo, cada 16 fotogramas porque te retrasaste un milisegundo), lo notarás: perderás varios fotogramas por segundo. También trabajarás para generar fotogramas que nunca se mostrarán, lo que desperdicia energía y tiempo de CPU que podrías dedicar a otras tareas en tu aplicación.
Las diferentes pantallas tienen distintas frecuencias de actualización: 60 Hz es común, pero algunos teléfonos tienen 59 Hz, algunas laptops bajan a 50 Hz en el modo de bajo consumo y algunos monitores de escritorio tienen 70 Hz.
Solemos enfocarnos en los fotogramas por segundo (FPS) cuando hablamos del rendimiento de renderización, pero la varianza puede ser un problema aún mayor. Nuestros ojos notan los pequeños tirones irregulares en la animación que puede producir una animación con un tiempo incorrecto.
La forma de obtener fotogramas de animación con el tiempo correcto es con requestAnimationFrame. Cuando usas esta API, le pides al navegador un fotograma de animación. Se llama a tu devolución de llamada cuando el navegador está a punto de producir un fotograma nuevo. Esto sucede sin importar cuál sea la frecuencia de actualización.
requestAnimationFrame también tiene otras propiedades interesantes:
- Las animaciones en las pestañas en segundo plano se pausan, lo que conserva los recursos del sistema y la duración de batería.
- Si el sistema no puede controlar la renderización con la frecuencia de actualización de la pantalla, puede limitar las animaciones y producir la devolución de llamada con menos frecuencia (por ejemplo, 30 veces por segundo en una pantalla de 60 Hz). Si bien esto reduce la velocidad de fotogramas a la mitad, mantiene la animación coherente y, como se indicó anteriormente, nuestros ojos están mucho más en sintonía con la variación que con la velocidad de fotogramas. Una frecuencia constante de 30 Hz se ve mejor que una de 60 Hz que pierde algunos fotogramas por segundo.
Ya se habló de requestAnimationFrame en todas partes, así que consulta artículos como este de Creative JS para obtener más información, pero es un primer paso importante para una animación fluida.
Presupuesto de fotogramas
Como queremos que haya un fotograma nuevo listo en cada actualización de pantalla, solo hay tiempo entre las actualizaciones para realizar todo el trabajo de creación de un fotograma nuevo. En una pantalla de 60 Hz, eso significa que tenemos alrededor de 16 ms para ejecutar todo el código JavaScript, realizar el diseño, pintar y todo lo que el navegador deba hacer para mostrar el fotograma. Esto significa que, si el JavaScript dentro de tu devolución de llamada requestAnimationFrame tarda más de 16 ms en ejecutarse, no podrás producir un fotograma a tiempo para la sincronización vertical.
16 ms no es mucho tiempo. Por suerte, las Herramientas para desarrolladores de Chrome pueden ayudarte a detectar si estás excediendo tu presupuesto de fotogramas durante la devolución de llamada de requestAnimationFrame.
Si abres la línea de tiempo de las Herramientas para desarrolladores y grabas esta animación en acción, verás rápidamente que superamos con creces el presupuesto cuando animamos. En Timeline, cambia a "Frames" y observa lo siguiente:
Esas devoluciones de llamada de requestAnimationFrame (rAF) tardan más de 200 ms. Ese es un orden de magnitud demasiado largo para marcar un fotograma cada 16 ms. Si abres una de esas devoluciones de llamada de rAF largas, se revela lo que sucede en el interior: en este caso, mucho diseño.
En el video de Paul, se explica con más detalle la causa específica del cambio de diseño (se lee scrollTop) y cómo evitarlo. Pero el punto aquí es que puedes profundizar en la devolución de llamada y, luego, investigar qué está tardando tanto.
Observa la latencia de fotogramas de 16 ms. Ese espacio en blanco en los fotogramas es el margen que tienes para realizar más trabajo (o permitir que el navegador realice el trabajo que necesita hacer en segundo plano). Ese espacio en blanco es algo bueno.
Otra fuente de bloqueos
La causa más importante de problemas cuando se intenta ejecutar animaciones con tecnología de JavaScript es que otros elementos pueden interferir con la devolución de llamada de rAF y, incluso, impedir que se ejecute. Incluso si tu devolución de llamada de rAF es ligera y se ejecuta en solo unos milisegundos, otras actividades (como procesar un XHR que acaba de llegar, ejecutar controladores de eventos de entrada o ejecutar actualizaciones programadas en un temporizador) pueden llegar de repente y ejecutarse durante cualquier período sin ceder. En los dispositivos móviles, a veces, el procesamiento de estos eventos puede tardar cientos de milisegundos, durante los cuales la animación se detendrá por completo. A esos problemas de animación los llamamos bloqueos.
No hay una solución mágica para evitar estas situaciones, pero hay algunas prácticas recomendadas de arquitectura para prepararte para el éxito:
- No realices mucho procesamiento en los controladores de entrada. Ejecutar mucho código JS o intentar reorganizar toda la página durante, por ejemplo, un controlador onscroll es una causa muy común de una terrible inestabilidad.
- Envía la mayor cantidad de procesamiento posible (es decir, todo lo que tarde mucho tiempo en ejecutarse) a tu devolución de llamada de rAF o a los Web Workers.
- Si envías trabajo a la devolución de llamada de rAF, intenta dividirlo en fragmentos para que solo proceses un poco cada fotograma o retrasarlo hasta que finalice una animación importante. De esta manera, puedes seguir ejecutando devoluciones de llamada de rAF cortas y animar sin problemas.
Para obtener un excelente instructivo sobre cómo enviar el procesamiento a las devoluciones de llamada de requestAnimationFrame en lugar de a los controladores de entrada, consulta el artículo de Paul Lewis Animaciones más eficientes, potentes y rápidas con requestAnimationFrame.
Animación de CSS
¿Qué es mejor que un JS ligero en tus devoluciones de llamada de eventos y rAF? No JS.
Anteriormente, dijimos que no hay una solución mágica para evitar interrumpir las devoluciones de llamada de rAF, pero puedes usar la animación CSS para evitar la necesidad de usarlas por completo. En Chrome para Android en particular (y otros navegadores están trabajando en funciones similares), las animaciones de CSS tienen la propiedad muy deseable de que el navegador a menudo puede ejecutarlas incluso si se está ejecutando JavaScript.
En la sección anterior sobre el jank, hay una declaración implícita: los navegadores solo pueden hacer una cosa a la vez. Esto no es estrictamente cierto, pero es una buena suposición de trabajo: en cualquier momento, el navegador puede ejecutar JS, realizar el diseño o pintar, pero solo una de estas acciones a la vez. Esto se puede verificar en la vista de línea de tiempo de las Herramientas para desarrolladores. Una de las excepciones a esta regla son las animaciones de CSS en Chrome para Android (y pronto en Chrome para computadoras de escritorio, aunque todavía no).
Cuando sea posible, usar una animación de CSS simplificará tu aplicación y permitirá que las animaciones se ejecuten sin problemas, incluso mientras se ejecuta JavaScript.
// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
rAF = window.requestAnimationFrame;
var degrees = 0;
function update(timestamp) {
document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
console.log('updated to degrees ' + degrees);
degrees = degrees + 1;
rAF(update);
}
rAF(update);
Si haces clic en el botón, JavaScript se ejecuta durante 180 ms, lo que provoca un jank. Sin embargo, si en cambio controlamos esa animación con animaciones de CSS, ya no se produce el jank.
(Recuerda que, en el momento de escribir este artículo, la animación CSS solo está libre de jank en Chrome para Android, no en Chrome para computadoras de escritorio).
/* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
#foo {
+animation-duration: 3s;
+animation-timing-function: linear;
+animation-animation-iteration-count: infinite;
+animation-animation-name: rotate;
}
@+keyframes: rotate; {
from {
+transform: rotate(0deg);
}
to {
+transform: rotate(360deg);
}
}
Para obtener más información sobre el uso de animaciones CSS, consulta artículos como este en MDN.
Conclusión
En resumen:
- Cuando se anima, es importante producir fotogramas para cada actualización de pantalla. La animación sincronizada con Vsync tiene un gran impacto positivo en la forma en que se siente una app.
- La mejor manera de obtener una animación sincronizada con la actualización de la pantalla en Chrome y otros navegadores modernos es usar la animación CSS. Cuando necesitas más flexibilidad que la que proporciona la animación de CSS, la mejor técnica es la animación basada en requestAnimationFrame.
- Para mantener las animaciones de rAF en buen estado, asegúrate de que otros controladores de eventos no interfieran en la ejecución de la devolución de llamada de rAF y mantén las devoluciones de llamada de rAF cortas (<15 ms).
Por último, la animación sincronizada con vsync no solo se aplica a las animaciones simples de la IU, sino también a las animaciones de Canvas2D, las animaciones de WebGL y hasta el desplazamiento en páginas estáticas. En el próximo artículo de esta serie, profundizaremos en el rendimiento del desplazamiento con estos conceptos en mente.
¡Que disfrutes la animación!