O Chrome 88 (janeiro de 2021) vai limitar muito os timers de JavaScript encadeados para páginas ocultas em condições específicas. Isso vai reduzir o uso da CPU, o que também reduz o uso da bateria. Há alguns casos extremos em que isso vai mudar o comportamento, mas os timers são usados com frequência em que uma API diferente seria mais eficiente e confiável.
Ok, isso foi muito técnico e um pouco ambíguo. Vamos lá…
Terminologia
Páginas ocultas
Geralmente, oculto significa que uma guia diferente está ativa ou que a janela foi minimizada, mas os navegadores podem considerar uma página oculta sempre que o conteúdo dela não estiver totalmente visível. Alguns navegadores vão mais longe do que outros, mas você pode sempre usar a API Page Visibility para rastrear quando o navegador achar que a visibilidade mudou.
Temporizadores JavaScript
Por timers do JavaScript, quero dizer setTimeout
e setInterval
, que permitem programar
um callback no futuro. Os timers são úteis e não
vão desaparecer, mas às vezes são usados para consultar o estado quando um evento é mais
eficiente e preciso.
Timers encadeados
Se você chamar setTimeout
na mesma tarefa que um callback setTimeout
, a segunda
invocação será "encadeada". Com setInterval
, cada iteração faz parte da
cadeia. Isso pode ser mais fácil de entender com o código:
let chainCount = 0;
setInterval(() => {
chainCount++;
console.log(`This is number ${chainCount} in the chain`);
}, 500);
E:
let chainCount = 0;
function setTimeoutChain() {
setTimeout(() => {
chainCount++;
console.log(`This is number ${chainCount} in the chain`);
setTimeoutChain();
}, 500);
}
Como o controle de taxa funciona
O estrangulamento ocorre em etapas:
Limitação mínima
Isso acontece com os timers programados quando qualquer uma das seguintes condições é verdadeira:
- A página está visível.
- A página fez ruídos nos últimos 30 segundos. Isso pode ser de qualquer uma das APIs de criação de som, mas uma faixa de áudio silenciosa não conta.
O timer não é limitado, a menos que o tempo limite solicitado seja menor que 4 ms e a contagem da cadeia seja 5 ou mais. Nesse caso, o tempo limite é definido como 4 ms. Isso não é novo. Os navegadores fazem isso há muitos anos.
Limitação
Isso acontece com os timers programados quando o limite mínimo não é aplicado e qualquer das seguintes condições é verdadeira:
- A contagem de cadeias é menor que 5.
- A página ficou escondida por menos de 5 minutos.
- O WebRTC está em uso. Especificamente, há um
RTCPeerConnection
com umRTCDataChannel
"aberto" ou umMediaStreamTrack
"ativo".
O navegador vai verificar os timers nesse grupo uma vez por segundo. Como eles são verificados apenas uma vez por segundo, os timers com um tempo limite semelhante serão agrupados, consolidando o tempo que a guia precisa para executar o código. Isso também não é novo. Os navegadores fazem isso há anos.
Limitação intensa
Aqui estão as novidades do Chrome 88. A limitação intensa acontece em timers que são programados quando nenhuma das condições de limitação mínima ou limitação se aplica e todas as condições a seguir são verdadeiras:
- A página está escondida há mais de cinco minutos.
- O número de cadeias é 5 ou maior.
- A página ficou em silêncio por pelo menos 30 segundos.
- O WebRTC não está em uso.
Nesse caso, o navegador vai verificar os timers nesse grupo uma vez a cada minuto. Assim como antes, isso significa que os timers serão agrupados nessas verificações minuto a minuto.
Alternativas
Geralmente, há uma alternativa melhor para um timer, ou os timers podem ser combinados com outra coisa para serem mais gentis com as CPUs e a duração da bateria.
Enquete de estado
Esse é o uso (in)correto mais comum de timers, em que eles são usados para verificar ou pesquisar continuamente se algo mudou. Na maioria dos casos, há um equivalente de envio, em que a coisa informa sobre a mudança quando ela acontece, para que você não precise verificar sempre. Verifique se há um evento que faz a mesma coisa.
Alguns exemplos:
- Se você precisar saber quando um elemento entra na viewport, use
IntersectionObserver
. - Se você precisar saber quando o tamanho de um elemento muda, use
ResizeObserver
. - Se você precisar saber quando o DOM muda, use
MutationObserver
ou talvez callbacks do ciclo de vida de elementos personalizados. - Em vez de consultar um servidor, considere usar sockets da Web, eventos enviados pelo servidor, mensagens push ou fluxos de busca.
- Se você precisar reagir a mudanças de estágio em áudio/vídeo, use eventos como
timeupdate
eended
ourequestVideoFrameCallback
se precisar fazer algo com cada frame.
Também há gatilhos de notificação se você quiser mostrar uma notificação em um determinado horário.
Animação
A animação é uma coisa visual, então ela não deve usar o tempo da CPU quando a página está escondida.
requestAnimationFrame
é muito melhor em programar trabalhos de animação do que os timers do JavaScript. Ele
sincroniza com a taxa de atualização do dispositivo, garantindo que você receba apenas um
callback por frame visível e que receba o tempo máximo para
criar esse frame. Além disso, requestAnimationFrame
vai esperar a página ficar
visível para não usar a CPU quando a página estiver oculta.
Se você puder declarar toda a animação com antecedência, use animações CSS ou a
API Web Animations. Elas
têm as mesmas vantagens que requestAnimationFrame
, mas o navegador pode realizar
otimizações adicionais, como composição automática, e geralmente são
mais fáceis de usar.
Se a animação tiver baixa taxa de frames (como um cursor piscando), os timers ainda
são a melhor opção no momento, mas você pode combiná-los com requestAnimationFrame
para aproveitar o melhor dos dois 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();
Teste
Essa mudança será ativada para todos os usuários do Chrome 88 (janeiro de 2021). No momento, ele está ativado para 50% dos usuários do Chrome Beta, Dev e Canary. Se quiser testar, use esta flag de linha de comando ao iniciar o Chrome Beta, Dev ou Canary:
--enable-features="IntensiveWakeUpThrottling:grace_period_seconds/10,OptOutZeroTimeoutTimersFromThrottling,AllowAggressiveThrottlingWithWebSocket"
O argumento grace_period_seconds/10
faz com que o limite intenso seja ativado
após 10 segundos da página ser oculta, em vez de 5 minutos,
facilitando a visualização do impacto do limite.
O futuro
Como os timers são uma fonte de uso excessivo de CPU, vamos continuar procurando
maneiras de limitar o uso sem interromper o conteúdo da Web e APIs que podem ser
adicionadas/alteradas para atender aos casos de uso. Pessoalmente, gostaria de eliminar a necessidade de
animationInterval
em favor de callbacks de animação de baixa frequência eficientes. Se
tiver dúvidas, entre em contato comigo no Twitter.
Foto do cabeçalho de Heather Zabriskie no Unsplash.