Limitación intensa de los cronómetros de JS en cadena a partir de Chrome 88

Chrome 88 (enero de 2021) limitará en gran medida los cronómetros de JavaScript en cadena para las páginas ocultas en condiciones particulares. Esto reducirá el uso de CPU, lo que también reducirá el uso de batería. Existen algunos casos extremos en los que esto cambiará el comportamiento, pero, por lo general, se usan temporizadores en los que una API diferente sería más eficiente y confiable.

Muy bien. Era bastante jerga, pero un poco ambigua. Analicemos este caso...

Terminología

Páginas ocultas

Por lo general, oculto significa que hay una pestaña diferente activa o que se minimizó la ventana, pero los navegadores pueden considerar que una página está oculta siempre que su contenido no sea totalmente visible. Algunos navegadores son más amplios que otros aquí, pero siempre puedes usar la API de visibilidad de páginas para hacer un seguimiento de cuándo el navegador considera que cambió la visibilidad.

Cronómetros de JavaScript

Con temporizadores de JavaScript me refiero a setTimeout y setInterval, que te permiten programar una devolución de llamada en el futuro. Los temporizadores son útiles y no desaparecen, pero a veces se usan para consultar el estado cuando un evento sería más eficiente y preciso.

Temporizadores en cadena

Si llamas a setTimeout en la misma tarea que una devolución de llamada setTimeout, la segunda invocación se “encadena”. Con setInterval, cada iteración es parte de la cadena. Esto podría ser más fácil de entender si utilizas código:

let chainCount = 0;

setInterval(() => {
  chainCount++;
  console.log(`This is number ${chainCount} in the chain`);
}, 500);

Y

let chainCount = 0;

function setTimeoutChain() {
  setTimeout(() => {
    chainCount++;
    console.log(`This is number ${chainCount} in the chain`);
    setTimeoutChain();
  }, 500);
}

Cómo funciona la limitación

La limitación ocurre en etapas:

Limitación mínima

Esto sucede con los temporizadores que se programan cuando se cumple cualquiera de las siguientes condiciones:

  • La página está visible.
  • La página hizo ruidos en los últimos 30 segundos. Puede provenir de cualquiera de las APIs de creación de sonido, pero no se tienen en cuenta las pistas de audio silenciosas.

El temporizador no está limitado, a menos que el tiempo de espera solicitado sea inferior a 4 ms y el recuento de cadenas sea de 5 o superior, en cuyo caso el tiempo de espera se establece en 4 ms. Esto no es nuevo; los navegadores lo han hecho durante muchos años.

Limitación

Esto sucede con los temporizadores que se programan cuando no se aplica la regulación mínima y se cumple cualquiera de las siguientes opciones:

  • El recuento de cadenas es inferior a 5.
  • La página ocultó por menos de 5 minutos.
  • WebRTC está en uso. Específicamente, hay un RTCPeerConnection con un RTCDataChannel "abierto" o un MediaStreamTrack "en vivo".

El navegador verificará los temporizadores de este grupo una vez por segundo. Dado que solo se verifican una vez por segundo, los temporizadores con un tiempo de espera similar se agruparán y consolidarán el tiempo que la pestaña necesita para ejecutar el código. Esto tampoco es nuevo; los navegadores lo han hecho de alguna manera durante años.

Limitación intensiva

Muy bien. Este es el nuevo bit en Chrome 88. La limitación intensiva ocurre con los cronómetros que se programan cuando no se aplica ninguna de las condiciones de regulación mínima o regulación, y se cumplen todas las siguientes condiciones:

  • La página ocultó por más de 5 minutos.
  • El recuento de cadenas es 5 o mayor.
  • La página ha estado en silencio durante al menos 30 segundos.
  • WebRTC no está en uso.

En este caso, el navegador verificará los cronómetros de este grupo una vez por minuto. Al igual que antes, esto significa que los cronómetros se agruparán en estas verificaciones minuto a minuto.

Soluciones alternativas

Por lo general, hay una mejor alternativa a un temporizador, o bien se pueden combinar los temporizadores con otra cosa para ser más amable con las CPU y la duración de batería.

Sondeo estatal

Este es el uso más común (incorrecto) de los cronómetros, y se usan para verificar o sondear continuamente si algo cambió. En la mayoría de los casos, hay un push equivalente, en el que el elemento te informa sobre el cambio cuando se produce, de modo que no tienes que seguir revisando. Observa si hay un evento que logre lo mismo.

Estos son algunos ejemplos:

También puedes usar los activadores de notificaciones si quieres mostrar una notificación en un momento determinado.

Animación

La animación es un elemento visual, por lo que no debería usar tiempo de CPU cuando la página está oculta.

requestAnimationFrame es mucho mejor para programar el trabajo de animación que los temporizadores de JavaScript. Se sincroniza con la frecuencia de actualización del dispositivo, lo que garantiza que solo obtengas una devolución de llamada por cada fotograma que se pueda mostrar y el tiempo máximo para construir ese fotograma. Además, requestAnimationFrame esperará a que la página sea visible, por lo que no usará ninguna CPU cuando la página esté oculta.

Si puedes declarar toda la animación por adelantado, considera usar animaciones de CSS o la API de animaciones web. Estas tienen las mismas ventajas que requestAnimationFrame, pero el navegador puede realizar optimizaciones adicionales, como la composición automática, y, en general, son más fáciles de usar.

Si la velocidad de fotogramas de tu animación es baja (como un cursor intermitente), los temporizadores son la mejor opción en este momento, pero puedes combinarlos con requestAnimationFrame para obtener lo mejor de ambos mundos:

function animationInterval(ms, signal, callback) {
  const start = document.timeline.currentTime;

  function frame(time) {
    if (signal.aborted) return;
    callback(time);
    scheduleFrame(time);
  }

  function scheduleFrame(time) {
    const elapsed = time - start;
    const roundedElapsed = Math.round(elapsed / ms) * ms;
    const targetNext = start + roundedElapsed + ms;
    const delay = targetNext - performance.now();
    setTimeout(() => requestAnimationFrame(frame), delay);
  }

  scheduleFrame(start);
}

Uso:

const controller = new AbortController();

// Create an animation callback every second:
animationInterval(1000, controller.signal, time => {
  console.log('tick!', time);
});

// And stop it:
controller.abort();

Prueba

Este cambio se habilitará para todos los usuarios de Chrome en Chrome 88 (enero de 2021). Actualmente, está habilitado para el 50% de los usuarios de Chrome Beta, Dev y Canary. Si deseas probarlo, usa esta marca de línea de comandos cuando inicies Chrome Beta, Dev o Canary:

--enable-features="IntensiveWakeUpThrottling:grace_period_seconds/10,OptOutZeroTimeoutTimersFromThrottling,AllowAggressiveThrottlingWithWebSocket"

El argumento grace_period_seconds/10 provoca una limitación intensa después de que se oculta la página por 10 segundos, en lugar de hacerlo durante los 5 minutos, lo que facilita ver el impacto de la limitación.

El futuro

Dado que los temporizadores son una fuente de uso excesivo de la CPU, seguiremos analizando las formas en que podemos limitarlos sin interrumpir el contenido web y las APIs que podemos agregar o cambiar para cumplir con los casos de uso. En mi caso, no me gustaría eliminar la necesidad de usar animationInterval para favorecer las devoluciones de llamada de animación de baja frecuencia eficientes. Si tienes alguna pregunta, comunícate con nosotros en Twitter.

Foto del encabezado de Heather Zabriskie en Unsplash.