Introducción
Deseas que tu app web se sienta responsiva y fluida cuando realices animaciones, transiciones y otros pequeños efectos de la IU. Asegurarse de que estos efectos no tengan bloqueos puede marcar la diferencia entre una sensación "nativa" o una torpe y poco pulida.
Este es el primero de una serie de artículos sobre la optimización del rendimiento de la renderización en el navegador. Para comenzar, explicaremos 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.
Presentamos la sincronización vertical
Es posible que los jugadores de PC estén familiarizados con este término, pero no es común en la Web: ¿qué es la sincronización vertical?
Piensa en 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 sincronización vertical) 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 del fotograma almacenado en 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 "seccionamiento".
Para obtener una animación fluida, necesitas que haya un fotograma nuevo listo cada vez que se actualice la pantalla. Esto tiene dos grandes implicaciones: el tiempo 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 la pantalla para completar un fotograma (~16 ms en una pantalla de 60 Hz) y quieres comenzar a producir el siguiente fotograma en cuanto se muestra 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 (que analizaremos en un momento), pero los más preocupantes son los siguientes:
- La resolución del temporizador de JavaScript es solo del orden de varios milisegundos.
- Los diferentes dispositivos tienen diferentes frecuencias de actualización.
Recuerda el problema de sincronización de fotogramas mencionado anteriormente: necesitas un fotograma de animación completo, terminado con cualquier JavaScript, manipulación de 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 próxima actualización de la pantalla, pero la variación en las tasas de actualización de la pantalla lo hace imposible con un temporizador fijo. Independientemente del intervalo del temporizador, saldrás lentamente del período de tiempo de un fotograma y, al final, perderás uno. Esto sucedería incluso si el temporizador se activara con precisión de milisegundos, lo que no sucederá (como descubrieron los desarrolladores). La resolución del temporizador varía según si la máquina está con 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 equivocaste por un milisegundo), lo notarás: perderás varios fotogramas por segundo. También deberás trabajar para generar fotogramas que nunca se muestren, lo que desperdicia energía y tiempo de la CPU que podrías dedicar a otras tareas en tu aplicación.
Las diferentes pantallas tienen diferentes 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 computadoras de escritorio tienen 70 Hz.
Cuando hablamos del rendimiento de la renderización, solemos enfocarnos en los fotogramas por segundo (FPS), pero la variación puede ser un problema aún mayor. Nuestros ojos notan los pequeños saltos irregulares en la animación que puede producir una animación con un tiempo inadecuado.
La forma de obtener fotogramas de animación con el tiempo correcto es con requestAnimationFrame
. Cuando usas esta API, le solicitas al navegador un fotograma de animación. Se llama a tu devolución de llamada cuando el navegador pronto producirá un marco nuevo. Esto sucede independientemente de la frecuencia de actualización.
requestAnimationFrame
también tiene otras propiedades interesantes:
- Las animaciones de las pestañas en segundo plano se detienen, lo que conserva los recursos del sistema y la duración de la batería.
- Si el sistema no puede controlar la renderización a la frecuencia de actualización de la pantalla, puede reducir la velocidad de 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 tasa de 30 Hz estable se ve mejor que una de 60 Hz que pierde algunos fotogramas por segundo.
requestAnimationFrame
ya se analizó en todos lados, así que consulta artículos como este de Creative JS para obtener más información al respecto, pero es un primer paso importante para suavizar la animación.
Presupuesto de fotogramas
Como queremos que haya un fotograma nuevo listo en cada actualización de pantalla, solo tenemos el tiempo entre las actualizaciones para hacer todo el trabajo de crear un fotograma nuevo. En una pantalla de 60 Hz, eso significa que tenemos alrededor de 16 ms para ejecutar todo JavaScript, realizar el diseño, pintar y cualquier otra tarea que el navegador tenga que hacer para mostrar el fotograma. Esto significa que, si el código JavaScript dentro de la devolución de llamada de requestAnimationFrame
tarda más de 16 ms en ejecutarse, no tienes ninguna esperanza de 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 superando tu presupuesto de fotogramas durante la devolución de llamada de requestAnimationFrame.
Si abrimos el cronograma de Dev Tools y tomamos una grabación de esta animación en acción, rápidamente se muestra que superamos el presupuesto en la animación. En la línea de tiempo, cambia a “Marcos” y observa lo siguiente:
Esas devoluciones de llamada de requestAnimationFrame (rAF) tardan más de 200 ms. Eso 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 rediseño (lee scrollTop
) y cómo evitarlo. Pero el punto aquí es que puedes analizar la devolución de llamada y descubrir qué es lo que está demorando tanto.
Observa los tiempos de fotogramas de 16 ms. Ese espacio en blanco en los marcos es el margen que tienes para hacer más trabajo (o permitir que el navegador haga el trabajo que debe hacer en segundo plano). Ese espacio en blanco es algo bueno.
Otra fuente de bloqueos
El mayor problema que se produce cuando se intentan ejecutar animaciones potenciadas por JavaScript es que otros elementos pueden interferir en la devolución de llamada de rAF y hasta impedir que se ejecute. Incluso si tu devolución de llamada de rAF es liviana y se ejecuta en solo unos pocos milisegundos, otras actividades (como procesar un XHR que acaba de ingresar, ejecutar controladores de eventos de entrada o ejecutar actualizaciones programadas en un temporizador) pueden aparecer de repente y ejecutarse durante cualquier período de tiempo sin generar rendimientos. En 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 bloqueos de animación los llamamos bloqueos.
No hay una solución mágica para evitar estas situaciones, pero existen algunas prácticas recomendadas de arquitectura que te ayudarán a tener éxito:
- No realices mucho procesamiento en los controladores de entrada. Hacer mucho código JS o intentar reorganizar toda la página durante, por ejemplo, un controlador onscroll es una causa muy común de bloqueos.
- Envía la mayor cantidad posible de procesamiento (es decir, todo lo que tarde mucho en ejecutarse) a tu devolución de llamada de rAF o a los trabajadores web.
- Si envías trabajo a la devolución de llamada de rAF, intenta dividirlo para que solo proceses un poco cada fotograma o deténgalo hasta que finalice una animación importante. De esta manera, puedes seguir ejecutando devoluciones de llamada de rAF cortas y animar sin problemas.
Si quieres 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 ágiles, más rápidas y más eficientes con requestAnimationFrame.
Animación de CSS
¿Qué es mejor que JS ligero en tus devoluciones de llamada de eventos y rAF? Sin JS.
Antes dijimos que no hay una solución mágica para evitar interrumpir las devoluciones de llamada de rAF, pero puedes usar la animación de CSS para evitar que sean necesarias 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 bloqueo, 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 uno a la vez. Esto se puede verificar en la vista de Rutas de Herramientas para desarrolladores. Una de las excepciones a esta regla son las animaciones CSS en Chrome para Android (y pronto en Chrome para computadoras, aunque aún no).
Cuando sea posible, usa una animación de CSS para 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 causa interrupciones. Sin embargo, si controlamos esa animación con animaciones de CSS, ya no se produce el bloqueo.
(Recuerda que, en el momento de escribir este artículo, la animación de CSS solo está libre de bloqueos en Chrome para Android, no en Chrome para computadoras).
/* 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 de 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 con sincronización vertical tiene un gran impacto positivo en la sensación que genera una app.
- La mejor manera de obtener una animación con vsync en Chrome y otros navegadores modernos es usar la animación de 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 se interpongan en la ejecución de la devolución de llamada de rAF y mantén las devoluciones de llamada de rAF breves (<15 ms).
Por último, la animación con vsync no solo se aplica a animaciones simples de la IU, sino también a animaciones de Canvas2D, de WebGL y hasta al desplazamiento en páginas estáticas. En el próximo artículo de esta serie, analizaremos el rendimiento del desplazamiento teniendo en cuenta estos conceptos.
¡Que disfrutes animando!