Chrome 88 (enero de 2021) reducirá considerablemente los temporizadores de JavaScript encadenados para las páginas ocultas en condiciones particulares. Esto reducirá el uso de la CPU, lo que también reducirá el uso de batería. Hay algunos casos extremos en los que esto cambiará el comportamiento, pero los temporizadores suelen usarse cuando una API diferente sería más eficiente y confiable.
Muy bien, eso fue bastante técnico y un poco ambiguo. Analicemos esto en mayor detalle…
Terminología
Páginas ocultas
Por lo general, oculto significa que hay una pestaña diferente activa o que la ventana se minimizó, pero los navegadores pueden considerar que una página está oculta cuando su contenido no es visible. Algunos navegadores van más allá que otros en este aspecto, pero siempre puedes usar la API de visibilidad de páginas para hacer un seguimiento de cuándo el navegador cree que cambió la visibilidad.
Temporizadores de JavaScript
Cuando me refiero a los cronómetros de JavaScript, me refiero a setTimeout
y setInterval
, que te permiten
programar una devolución de llamada en algún momento en el futuro. Los temporizadores son útiles y no desaparecerán, pero a veces se usan para sondear el estado cuando un evento sería más eficiente y preciso.
Temporizadores encadenados
Si llamas a setTimeout
en la misma tarea que una devolución de llamada de setTimeout
, la segunda
invocación se "encadena". Con setInterval
, cada iteración forma parte de la cadena. Esto puede ser más fácil de entender con 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 el control de velocidad
La limitación se produce en etapas:
Regulación mínima
Esto sucede con los temporizadores programados cuando cualquiera de las siguientes condiciones es verdadera:
- La página es visible.
- La página emitió ruidos en los últimos 30 segundos. Esto puede ser de cualquiera de las APIs de creación de sonido, pero no se cuenta una pista de audio silenciosa.
El temporizador no se limita, a menos que el tiempo de espera solicitado sea inferior a 4 ms y el recuento de cadenas sea 5 o superior, en cuyo caso el tiempo de espera se establece en 4 ms. Esto no es nuevo, ya que los navegadores lo hacen desde hace muchos años.
Regulación
Esto sucede con los temporizadores programados cuando no se aplica la limitación mínima y se cumple cualquiera de las siguientes condiciones:
- El registro de cadenas es inferior a 5.
- La página se ocultó hace menos de 5 minutos.
- WebRTC está en uso. Específicamente, hay un
RTCPeerConnection
con unRTCDataChannel
“abierto” o unMediaStreamTrack
“en vivo”.
El navegador verificará los temporizadores de este grupo una vez por segundo. Debido a que solo se verifican una vez por segundo, los temporizadores con un tiempo de espera similar se agruparán, lo que consolidará el tiempo que la pestaña necesita para ejecutar el código. Esto tampoco es nuevo, ya que los navegadores lo hacen hasta cierto punto desde hace años.
Limitación intensiva
Muy bien, esta es la novedad de Chrome 88. La limitación intensiva se produce en los temporizadores programados cuando no se aplica ninguna de las condiciones de limitación mínima o limitación, y todas las siguientes condiciones son verdaderas:
- La página estuvo oculta durante más de 5 minutos.
- El recuento de cadenas es 5 o mayor.
- La página estuvo en silencio durante al menos 30 segundos.
- WebRTC no está en uso.
En este caso, el navegador verificará los temporizadores de este grupo una vez por minuto. Al igual que antes, esto significa que los temporizadores se agruparán en estas verificaciones minuto a minuto.
Soluciones alternativas
Por lo general, hay una mejor alternativa a un temporizador, o bien los temporizadores se pueden combinar con algo más para ser más amigables con las CPUs y la duración de batería.
Sondeo de estado
Este es el uso más común (y desaconsejado) de los temporizadores, en el que se usan para verificar o sondear de forma continua si algo cambió. En la mayoría de los casos, hay un equivalente de push, en el que el elemento te informa sobre el cambio cuando ocurre, por lo que no tienes que seguir verificando. Comprueba si hay un evento que logre lo mismo.
Estos son algunos ejemplos:
- Si necesitas saber cuándo un elemento entra en el viewport, usa
IntersectionObserver
. - Si necesitas saber cuándo cambia de tamaño un elemento, usa
ResizeObserver
. - Si necesitas saber cuándo cambia el DOM, usa
MutationObserver
o, tal vez, callbacks del ciclo de vida de elementos personalizados. - En lugar de sondear un servidor, considera usar sockets web, eventos enviados por el servidor, mensajes push o flujos de actualización.
- Si necesitas reaccionar a los cambios de etapa en el audio o el video, usa eventos como
timeupdate
yended
, orequestVideoFrameCallback
si necesitas hacer algo con cada fotograma.
También hay 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 recibas una devolución de llamada por fotograma visible y que tengas la cantidad máxima de tiempo para construir ese fotograma. Además, requestAnimationFrame
esperará a que la página sea visible para no usar CPU cuando esté oculta.
Si puedes declarar toda la animación de antemano, considera usar animaciones de CSS o la API de animaciones web. Estos tienen las mismas ventajas que requestAnimationFrame
, pero el navegador puede realizar optimizaciones adicionales, como la composición automática, y, por lo general, son más fáciles de usar.
Si tu animación tiene una velocidad de fotogramas baja (como un cursor parpadeante), los temporizadores siguen siendo 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á habilitada para el 50% de los usuarios de Chrome beta, Canary y para desarrolladores. Si quieres 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
hace que se active una limitación intensa después de 10 segundos de que se oculte la página, en lugar de los 5 minutos completos, 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 regularlos sin interrumpir el contenido web y las APIs que podemos agregar o cambiar para cumplir con los casos de uso. Personalmente, me gustaría eliminar la necesidad de animationInterval
en favor de devoluciones de llamada de animación de baja frecuencia eficientes. Si tienes alguna pregunta, comunícate conmigo por Twitter.
Foto del encabezado de Heather Zabriskie en Unsplash.