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

Tom Wiltzius
Tom Wiltzius

Introducción

Tu app web debe ser responsiva y fluida cuando realices animaciones, transiciones y otros efectos pequeños de la IU. Asegurarse de que estos efectos no tengan bloqueos puede marcar la diferencia entre una cultura “nativa” o algo torpe y sin pulir.

Este es el primero de una serie de artículos que abordan la optimización del rendimiento de la renderización en el navegador. Para comenzar, abordaremos por qué la animación fluida es difícil y qué debe suceder 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 V-sync

Es posible que los gamers de PC ya estén familiarizados con este término, pero es poco frecuente en la Web: ¿qué es v-sync?

Considera la pantalla de tu teléfono: se actualiza en intervalos regulares, generalmente (pero no siempre) alrededor de 60 veces por segundo. V-sync (o sincronización vertical) hace referencia a la práctica de generar nuevos fotogramas solo entre actualizaciones de pantalla. Se puede pensar en esto como una condición de carrera entre el proceso que escribe los datos en el búfer de la pantalla y el sistema operativo que lee esos datos para colocarlos en la pantalla. Queremos que el contenido de los fotogramas 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, debes tener un fotograma nuevo listo cada vez que se actualice la pantalla. Esto tiene dos grandes implicaciones: la latencia de fotogramas (es decir, cuándo el fotograma debe estar listo) y el presupuesto de fotogramas (es decir, el tiempo que tiene el navegador para producir un fotograma). Solo tienes tiempo entre las actualizaciones de pantalla para completar un fotograma (~16 ms en una pantalla de 60 Hz), y quieres comenzar a producir el siguiente fotograma apenas se colocó el último 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 (analizaremos más en un minuto), pero las siguientes son las que más preocupan:

  • La resolución del temporizador de JavaScript es solo 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, manipulaciones del DOM, diseño, pintura, etc., para estar listo antes de la siguiente actualización de la pantalla. La baja resolución del temporizador puede dificultar la finalización de los fotogramas de animación antes de la siguiente actualización de la pantalla, pero la variación en la frecuencia de actualización de la pantalla hace que sea imposible 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 perdiendo uno. Esto sucede incluso si el temporizador se activa con una precisión de milisegundos, pero no lo hará (como lo descubrieron los desarrolladores). La resolución del temporizador varía según si la máquina está con batería o si está enchufada, puede verse afectada por las pestañas en segundo plano que acaparan los recursos, etc. Incluso si esto es poco frecuente (por ejemplo, cada 16 fotogramas porque estuviste fuera de un milisegundo), notarás: verás varios fotogramas por segundo. También trabajarás para generar marcos que nunca se muestran, lo que desperdicia energía y tiempo de CPU que podrías dedicar a otras cosas en tu aplicación.

Las diferentes pantallas tienen distintas frecuencias de actualización: 60 Hz es común, pero algunos teléfonos son de 59 Hz, algunas laptops disminuyen a 50 Hz en modo de bajo consumo y algunos monitores de escritorio son de 70 Hz.

Por lo general, nos enfocamos en los fotogramas por segundo (FPS) cuando hablamos del rendimiento de la renderización, pero la variación puede ser un problema aún mayor. Nuestros ojos notan los pequeños enganches irregulares en la animación que puede producir una animación mal sincronizada.

La forma de obtener fotogramas de animación programados correctamente es con requestAnimationFrame. Cuando usas esta API, le solicitas al navegador un fotograma de animación. Se llamará a tu devolución de llamada cuando el navegador pronto produzca un nuevo fotograma. 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 ahorra 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 limitar las animaciones y producir la devolución de llamada con menor 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 coherencia de la animación. Como se mencionó anteriormente, nuestros ojos están mucho más en sintonía con la variación que la velocidad de fotogramas. Un 30 Hz estable luce mejor que uno de 60 Hz que falla algunos fotogramas por segundo.

requestAnimationFrame ya se habló en muchos lugares, por lo que debes consultar 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 del marco

Como queremos un nuevo marco listo en cada actualización de la pantalla, solo hay tiempo entre las actualizaciones para hacer todo el trabajo de crear un nuevo marco. En una pantalla de 60 Hz, tenemos unos 16 ms para ejecutar todo el código JavaScript, realizar diseños, pintar y hacer lo que sea que deba hacer el navegador para capturar el marco. Esto significa que, si el JavaScript dentro de tu devolución de llamada requestAnimationFrame tarda más de 16 ms en ejecutarse, no tienes esperanzas de producir un fotograma a tiempo para la sincronización v.

16 ms no es mucho tiempo. Afortunadamente, las Herramientas para desarrolladores de Chrome pueden ayudarte a detectar si el porcentaje de fotogramas es limitado durante la devolución de llamada requestAnimationFrame.

Abrir el cronograma de las Herramientas para desarrolladores y grabar esta animación en acción rápidamente muestra que estamos muy por encima del presupuesto establecido para la animación. En Rutas, cambia a “Fotogramas” y echa un vistazo:

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. Eso es un orden de magnitud demasiado largo para registrar un fotograma cada 16 ms. La apertura de una de esas largas devoluciones de llamada de rAF revela lo que sucede en el interior: en este caso, hay mucho diseño.

En el video de Paul, se detalla la causa específica del cambio de diseño (se lee scrollTop) y se explica cómo evitarlo. Pero el punto aquí es que puedes sumergirte en la devolución de llamada e investigar 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 lo haga en segundo plano). Ese espacio en blanco es algo bueno.

Otra fuente de bloqueos

La principal causa de problemas cuando se intentan ejecutar animaciones con tecnología JavaScript es que pueden interferir con la devolución de llamada de rAF e, incluso, no se ejecute. Incluso si tu devolución de llamada de rAF es eficiente y se ejecuta en pocos pasos milisegundos, otras actividades (como procesar un XHR que acaba de llegar, ejecutar controladores de eventos de entrada o actualizaciones programadas en un cronómetro) de repente se ponen y ejecutan durante cualquier período sin ceder. En dispositivos móviles que los dispositivos a veces procesan estos eventos demoran cientos de milisegundos y tu animación quedará completamente detenida. Los llamamos La animación genera un bloqueo.

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

  • No realices mucho procesamiento en los controladores de entrada. Hacer mucho JS o intentar reorganizar toda la página, p.ej., un controlador onscroll es una causa muy común de bloqueos terribles.
  • Envía la mayor cantidad de procesamiento (es decir, cualquier elemento que tarde mucho tiempo en ejecutarse) a la devolución de llamada de rAF o a los Web Workers como sea posible.
  • Si envías trabajo a la devolución de llamada de rAF, intenta dividirlo para que solo proceses un poco cada fotograma o retrasalo hasta después de que finalice una animación importante. De esta manera, puedes seguir ejecutando devoluciones de llamada de rAF breves y realizar animaciones fluidas.

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

Animación CSS

¿Qué es mejor que JS básico en tus devoluciones de llamada de eventos y rAF? No se requiere JS.

Anteriormente, dijimos que no hay soluciones para evitar interrumpir las devoluciones de llamada de rAF, pero puedes usar la animación de CSS para evitar la necesidad de usarlas por completo. En Chrome para Android en particular (y otros navegadores están trabajando con funciones similares), las animaciones CSS tienen la propiedad más conveniente de que el navegador a menudo pueda ejecutarlas incluso si se está ejecutando JavaScript.

En la sección anterior, hay una declaración implícita sobre el bloqueo: los navegadores solo pueden hacer una acción a la vez. Esto no es estrictamente cierto, pero es una buena suposición: en cualquier momento, el navegador puede ejecutar JS, realizar diseños 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 en Chrome para computadoras de escritorio, aunque todavía no).

Siempre que sea posible, usar 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. Pero si, en cambio, impulsamos esa animación con animaciones de CSS, el bloqueo dejará de ocurrir.

(Recuerda que, al momento de redactar este documento, la animación CSS solo no se bloquea en Chrome para Android, no en la versión de escritorio de Chrome).

  /* 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 pocas palabras:

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

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

¡Que disfrutes con la animación!

Referencias