Prevención de bloqueos para mejorar el rendimiento de la renderización

Tom Wiltzius
Tom Wiltzius

Introducción

La app web debería ser responsiva y fluida cuando realice animaciones, transiciones y otros efectos pequeños de la IU. Asegurarse de que estos efectos no presenten bloqueos puede marcar la diferencia entre una apariencia "nativa" o una tosca y desagradable.

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 crear animaciones fluidas y qué debe suceder para lograrlo, además de algunas prácticas recomendadas simples. Muchas de estas ideas se presentaron originalmente en "Jank Busters", una charla que Nat Duca y yo dimos en la charla de Google I/O (video) este año.

Presentamos V-sync

Los gamers de PC podrían estar familiarizados con este término, pero es poco común en la Web: ¿qué es v-sync?

Ten en cuenta la pantalla de tu teléfono: se actualiza en intervalos regulares, por lo general (aunque no siempre), unas 60 veces por segundo. La sincronización con V (o sincronización vertical) hace referencia a la práctica de generar nuevos marcos solo entre actualizaciones de pantalla. Podrías pensar en esto como una condición de carrera entre el proceso que escribe datos en el búfer de la pantalla y el sistema operativo que lee esos datos para mostrarlos en la pantalla. Queremos que el contenido de los marcos almacenados 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: la latencia 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 actualizaciones de pantalla para completar un fotograma (~16 ms en una pantalla de 60 Hz) y quieres comenzar a producir el siguiente fotograma en cuanto el último se haya colocado en la pantalla.

El tiempo lo es todo: requestAnimationFrame

Muchos desarrolladores web usan setInterval o setTimeout cada 16 milisegundos para crear animaciones. Este es un problema por varias razones (y las analizaremos en breve), pero son las más importantes:

  • La resolución del temporizador desde JavaScript solo es de varios milisegundos.
  • Los diferentes dispositivos tienen distintas frecuencias de actualización.

Recuerda el problema de latencia de fotogramas mencionado anteriormente: necesitas un fotograma de animación completo, terminado con JavaScript, manipulación, diseño o pintura de DOM, etc., para estar listo antes de la siguiente actualización de la pantalla. Cuando la resolución del temporizador es baja, puede dificultar la tarea de completar los fotogramas de animación antes de la próxima actualización de la pantalla, pero la variación en las frecuencias de actualización de la pantalla hace que sea imposible hacerlo con un temporizador fijo. No importa cuál sea el intervalo del temporizador, te saldrás lentamente de la ventana de sincronización de un fotograma y terminarás descartando uno. Esto sucede incluso si el temporizador se activa con milisegundos de precisión, lo cual no (como lo descubrieron los desarrolladores); la resolución del temporizador varía según si la máquina está enchufada o con batería, 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 estuvo un milisegundo), notarás que perderás varios fotogramas por segundo. También harás el trabajo de generar marcos que nunca se muestren, lo que desperdicia energía y tiempo de CPU que podrías dedicar a realizar 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 disminuyen a 50 Hz en modo de bajo consumo y algunos monitores de escritorio son de 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 e irregulares enganches en la animación que una animación mal programada puede producir.

La forma de obtener fotogramas de animación con los tiempos correctos 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 nuevo fotograma. Esto sucede sin importar la frecuencia de actualización.

requestAnimationFrame también tiene otras propiedades buenas:

  • Se pausan las animaciones en las pestañas en segundo plano, lo que ahorra recursos del sistema y duración de batería.
  • Si el sistema no puede controlar la renderización a 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 disminuye a la mitad la velocidad de fotogramas, mantiene la coherencia de la animación 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. Un valor constante de 30 Hz se ve mejor que uno de 60 Hz, pero pierde algunos fotogramas por segundo.

requestAnimationFrame ya se discutió en muchos lugares, 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 optimizar la animación.

Presupuesto de marco

Como queremos que un fotograma nuevo esté listo en cada actualización de la pantalla, solo hay tiempo entre actualizaciones para hacer todo el trabajo de crear un nuevo fotograma. En una pantalla de 60 Hz, eso significa que tenemos alrededor de 16 ms para ejecutar todo JavaScript, realizar el diseño, la pintura y lo que el navegador tenga que hacer para obtener el marco. Esto significa que, si el código JavaScript de tu devolución de llamada a requestAnimationFrame tarda más de 16 ms en ejecutarse, no puedes producir un fotograma a tiempo para v-sync.

16 ms no es mucho tiempo. Afortunadamente, las herramientas para desarrolladores de Chrome pueden ayudarte a identificar si estás superando tu presupuesto de fotogramas durante la devolución de llamada requestAnimationFrame.

Si abres la línea de tiempo de Herramientas para desarrolladores y tomas una grabación de esta animación en acción rápidamente, podremos ver que superamos el presupuesto durante la animación. En Rutas, cambia a "Fotogramas" y consulta lo siguiente:

Una demostración con demasiado diseño
Una demostración con demasiado diseño

Esas devoluciones de llamada requestAnimationFrame (rAF) tardan más de 200 ms. Es un orden de magnitud demasiado largo para marcar un fotograma cada 16 ms. Abrir una de esas devoluciones de llamada de rAF largas revela lo que sucede adentro: en este caso, mucho diseño.

En el video de Paul, se explica con más detalle la causa específica del rediseño (se lee scrollTop) y cómo evitarlo. Sin embargo, el punto aquí es que puedes profundizar en la devolución de llamada e investigar por qué está tardando tanto.

Una demostración actualizada con un diseño mucho más reducido
Una demostración actualizada con un diseño mucho más reducido

Observa las latencias de fotogramas de 16 ms. Ese espacio en blanco en los marcos es el margen que tienes para hacer más trabajo (o dejar que el navegador haga el trabajo que debe hacer en segundo plano). Ese espacio en blanco es algo bueno.

Otra fuente de bloqueos

La mayor causa del problema cuando intentas ejecutar animaciones con tecnología JavaScript es que otros elementos pueden obstaculizar la devolución de llamada de rAF e incluso impedir que se ejecute. Incluso si la devolución de llamada de rAF es simple y se ejecuta en pocos milisegundos, otras actividades (como procesar una XHR que acaba de llegar, ejecutar controladores de eventos de entrada o ejecutar actualizaciones programadas en un temporizador) pueden entrar y ejecutarse de repente durante cualquier período sin rendirse. En los dispositivos móviles, a veces, el procesamiento de estos eventos puede tardar cientos de milisegundos y durante ese tiempo la animación se detendrá por completo. A esos enganches de animación los llamamos bloqueo.

No hay una solución mágica para evitar estas situaciones, pero hay algunas prácticas recomendadas de arquitectura que te permitirán prepararte para el éxito:

  • No realices mucho procesamiento en los controladores de entrada. Usar mucho JS o intentar reorganizar toda la página durante, p.ej., con un controlador onscroll, es muy común que se produzca un terrible bloqueo.
  • Envía tantos procesamientos (lecturas: cualquier cosa que lleve mucho tiempo en ejecutarse) a tu devolución de llamada de rAF o a los Trabajadores web como sea posible.
  • Si insertas el trabajo en la devolución de llamada de rAF, intenta fragmentarlo de modo que solo proceses un poco cada fotograma o retrasalo hasta que finalice una animación importante. De esta manera, puedes continuar ejecutando devoluciones de llamada de rAF breves y realizar una animación fluida.

Para ver un excelente instructivo sobre cómo enviar el procesamiento a devoluciones de llamada de requestAnimationFrame en lugar de controladores de entrada, consulta el artículo de Paul Lewis: Leaner, Meaner, Faster Animations with requestAnimationFrame.

Animación CSS

¿Qué es mejor que JS liviano en tu evento y devoluciones de llamada de rAF? Sin JS.

Anteriormente, mencionamos que no hay soluciones milagrosas para evitar interrumpir las devoluciones de llamada de rAF, pero puedes usar la animación de CSS para evitarlas por completo. En particular, en Chrome para Android (y en otros navegadores que están trabajando con características similares), las animaciones de CSS tienen la propiedad muy deseable de que el navegador a menudo puede ejecutarlas incluso cuando se ejecuta JavaScript.

En la sección anterior, se incluye una declaración implícita sobre los bloqueos: los navegadores solo pueden realizar una acción a la vez. Esto no es estrictamente cierto, pero es una buena suposición funcional: en cualquier momento el navegador puede ejecutar JS, realizar diseño o pintar, pero solo uno a la vez. Esto se puede verificar en la vista de cronograma de las Herramientas para desarrolladores. Una de las excepciones a esta regla son las animaciones de CSS en Chrome para Android (y, pronto, la versión de Chrome para computadoras de escritorio, aunque todavía no).

Cuando sea posible, el uso de una animación CSS simplifica tu aplicación y permite 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 ejecutará durante 180 ms, lo que provocará el bloqueo. Sin embargo, si impulsamos la animación con animaciones de CSS, el bloqueo dejará de ocurrir.

(Recuerda que, en el momento de la redacción de este documento, la animación de CSS solo está libre de bloqueos 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 de CSS, consulta artículos como este sobre MDN.

Conclusión

La breve es:

  1. Cuando se realizan animaciones, es importante producir fotogramas para cada actualización de la pantalla. La animación de Vsync tiene un enorme impacto positivo en la sensación que transmite una app.
  2. La mejor manera de obtener animación vsync en Chrome y en otros navegadores modernos es usar animación CSS. Cuando necesitas más flexibilidad de la que proporciona la animación CSS, la mejor técnica es la animación basada en requestAnimationFrame.
  3. Para que las animaciones de rAF estén en buen estado y en buen estado, asegúrate de que otros controladores de eventos no interfieran en la ejecución de tu devolución de llamada de rAF y mantén cortas las devoluciones de llamada de rAF (<15 ms).

Por último, la animación vsyncd no solo se aplica a las animaciones simples de la IU, sino también a la animación Canvas2D, la animación WebGL e incluso el desplazamiento en páginas estáticas. En el siguiente artículo de esta serie, profundizaremos en el rendimiento del desplazamiento teniendo en cuenta estos conceptos.

¡Suerte con las animaciones!

Referencias