O Chrome 88 (janeiro de 2021) limitará os timers do JavaScript encadeados para páginas ocultas em determinadas condições. Isso reduzirá o uso da CPU, o que também reduzirá o uso da bateria. Há alguns casos extremos em que isso muda o comportamento, mas os timers são frequentemente usados, quando uma API diferente seria mais eficiente e mais confiável.
Certo, esse era um jargão meio ambíguo. Vamos lá...
Terminologia
Páginas ocultas
Geralmente, oculta 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 está totalmente não visível. Alguns navegadores vão mais além do que outros aqui, mas você sempre pode usar a API de visibilidade de páginas para acompanhar quando o navegador acredita que houve uma mudança na visibilidade.
Timers do JavaScript
Por Temporizadores do JavaScript quero dizer setTimeout
e setInterval
, que permitem programar um callback em algum momento no futuro. Os timers são úteis e não
são desativados, mas às vezes são usados para pesquisar o estado em que um evento seria mais
eficiente e mais 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 a limitação funciona
A limitação acontece em etapas:
Limitação mínima
Isso acontece com timers programados quando qualquer uma das seguintes condições é verdadeira:
- A página está visível.
- A página fez barulho nos últimos 30 segundos. Isso pode ser de qualquer uma das APIs de produçã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 4ms e a contagem de cadeias seja 5 ou maior. Nesse caso, o tempo limite é definido como 4ms. Isso não é novo, já que os navegadores já fazem isso há muitos anos.
Limitação
Isso acontece com os timers programados quando a limitação mínima não se aplica, e qualquer uma das seguintes condições é verdadeira:
- A contagem de cadeias for menor que 5.
- A página está oculta há menos de cinco minutos.
- O WebRTC está em uso. Especificamente, há um
RTCPeerConnection
com umRTCDataChannel
"aberto" ou umMediaStreamTrack
"ativo".
O navegador verificará os timers neste grupo uma vez por segundo. Como eles são verificados apenas uma vez por segundo, os timers com um tempo limite semelhante sã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, até certo ponto.
Limitação intensa
Ok, aqui está a nova parte do Chrome 88. A limitação intensa acontece com os timers 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á oculta há mais de cinco minutos.
- A contagem de cadeias é 5 ou mais.
- A página ficou em silêncio por pelo menos 30 segundos.
- O WebRTC não está em uso.
Nesse caso, o navegador 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 ao timer, ou os timers podem ser combinados com algo mais para serem mais favoráveis às CPUs e à duração da bateria.
Pesquisas de estado
Esse é o uso mais comum (erro) de timers, em que eles são usados para verificar ou pesquisar continuamente para ver se algo mudou. Na maioria dos casos, há um push equivalente, em que a coisa informa sobre a mudança quando ela acontece, de modo que você não precisa continuar verificando. Verifique se há um evento que realiza o mesmo resultado.
Alguns exemplos:
- Se você precisar saber quando um elemento entra na janela de visualização, use
IntersectionObserver
. - Se você precisar saber quando um elemento mudar de tamanho, use
ResizeObserver
. - Se você precisa saber quando o DOM muda, use
MutationObserver
ou talvez callbacks do ciclo de vida do elemento personalizado. - Em vez de pesquisar um servidor, considere soquetes da Web, eventos enviados pelo servidor, mensagens push ou buscar streams.
- Se você precisar reagir a mudanças de fase em áudio/vídeo, use eventos como
timeupdate
eended
ourequestVideoFrameCallback
, se precisar fazer algo com cada frame.
Há também acionadores de notificação, caso você queira mostrar uma notificação em um momento específico.
Animação
A animação é visual e não pode usar o tempo de CPU quando a página está oculta.
requestAnimationFrame
é muito melhor para programar trabalhos de animação do que timers do JavaScript. Ela
é sincronizada com a taxa de atualização do dispositivo, garantindo que você receba apenas um
callback por frame que possa ser exibido e que você tenha o tempo máximo para
criar esse frame. Além disso, o requestAnimationFrame
vai aguardar até que a página fique
visível para que não use nenhuma CPU quando a página estiver oculta.
Se você puder declarar toda a animação antecipadamente, considere usar animações CSS ou a
API Web Animation. Elas
têm as mesmas vantagens de requestAnimationFrame
, mas o navegador pode realizar
outras otimizações, como a composição automática, e elas geralmente
são mais fáceis de usar.
Se a animação tiver um frame rate baixo (como um cursor piscando), os timers ainda
são a melhor opção no momento, mas eles podem ser combinados com requestAnimationFrame
para ter 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();
testes
Essa mudança será ativada para todos os usuários no Chrome 88 (janeiro de 2021). No momento, ela está ativada para 50% dos usuários do Chrome Beta, Dev e Canary. Se você quiser testá-lo, use esta sinalização 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 a limitação intensa seja iniciada após 10 segundos da página sendo ocultada, em vez dos 5 minutos completos, facilitando a visualização do impacto da limitação.
O futuro
Como os timers são uma fonte de uso excessivo da CPU, continuaremos a analisar
as maneiras de limitá-los sem corromper o conteúdo da Web e as APIs que podemos
adicionar/mudar para atender a casos de uso. Pessoalmente, quero eliminar a necessidade de
animationInterval
em favor de callbacks de animação de baixa frequência eficientes. Se
tiver alguma dúvida, entre em contato comigo no Twitter.
Foto de capa de Heather Zabriskie no Unsplash.