Limitazione intensiva dei timer JS concatenati a partire da Chrome 88

Jake Archibald
Jake Archibald

Chrome 88 (gennaio 2021) ridurrà notevolmente la frequenza dei timer JavaScript incatenati per le pagine nascoste in determinate condizioni. In questo modo si riduce l'utilizzo della CPU, il che consente di ridurre anche l'utilizzo della batteria. Esistono alcuni casi limite in cui questo comportamento cambia, ma i timer vengono spesso utilizzati quando un'API diversa sarebbe più efficiente e affidabile.

OK, il gergo era piuttosto pesante e un po' ambiguo. Scopriamolo…

Terminologia

Pagine nascoste

In genere, nascosta significa che è attiva una scheda diversa o che la finestra è stata minimizzata, ma i browser potrebbero considerare una pagina nascosta ogni volta che i suoi contenuti non sono totalmente visibili. Alcuni browser vanno oltre, ma puoi sempre utilizzare l'API Page Visibility per monitorare quando il browser ritiene che la visibilità sia cambiata.

Timer JavaScript

Per timer JavaScript intendo setTimeout e setInterval, che ti consentono di pianificare un callback in un secondo momento. I timer sono utili e non scompariranno, ma a volte vengono utilizzati per eseguire il polling dello stato quando un evento sarebbe più efficiente e preciso.

Timer incatenati

Se chiami setTimeout nella stessa attività come callback di setTimeout, la seconda chiamata è "in catena". Con setInterval, ogni iterazione fa parte della catena. Potrebbe essere più facile da capire con il codice:

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);
}

Come funziona la limitazione

La limitazione avviene in più fasi:

Limitazione minima

Questo accade ai timer programmati quando una delle seguenti condizioni è vera:

  • La pagina sia visibile.
  • La pagina ha emesso rumori negli ultimi 30 secondi. Può essere di qualsiasi API di creazione di suoni, ma una traccia audio silenziosa non viene conteggiata.

Il timer non viene limitato, a meno che il timeout richiesto sia inferiore a 4 ms e il numero di catene sia pari o superiore a 5, nel qual caso il timeout viene impostato su 4 ms. Questo non è un fatto nuovo; i browser lo fanno da molti anni.

Limitazione

Questo accade ai timer pianificati quando non viene applicato il throttling minimo e una qualsiasi delle seguenti condizioni è vera:

  • Il conteggio delle catene è inferiore a 5.
  • La pagina è stata nascosta per meno di 5 minuti.
  • WebRTC è in uso. Nello specifico, è presente un RTCPeerConnection con un RTCDataChannel "aperto" o un MediaStreamTrack "in diretta".

Il browser controllerà i timer in questo gruppo una volta ogni secondo. Poiché vengono controllati solo una volta al secondo, i timer con un timeout simile vengono raggruppati, consolidando il tempo necessario alla scheda per eseguire il codice. Anche questo non è nuovo; i browser lo fanno in qualche misura da anni.

Limitazione intensa

Ok, ecco la novità di Chrome 88. Il throttling intensivo si verifica per i timer pianificati quando non si applicano le condizioni di throttling minimo o throttling e tutte le seguenti condizioni sono vere:

  • La pagina è stata nascosta per più di 5 minuti.
  • Il conteggio delle catene è pari o superiore a 5.
  • La pagina è inattiva da almeno 30 secondi.
  • WebRTC non è in uso.

In questo caso, il browser controllerà i timer in questo gruppo una volta ogni minuto. Come in precedenza, questo significa che i timer verranno raggruppati in questi controlli minuziosi.

Soluzioni alternative

Di solito esiste un'alternativa migliore a un timer oppure i timer possono essere combinati con qualcos'altro per risparmiare sulle CPU e sulla durata della batteria.

Sondaggio sullo stato

Questo è il (cattivo) uso più comune dei timer, che vengono utilizzati per controllare o effettuare sondaggi continuamente per vedere se è cambiato qualcosa. Nella maggior parte dei casi esiste un equivalente di push, in cui l'elemento ti informa della modifica quando si verifica, quindi non devi continuare a controllare. Controlla se esiste un evento che ottiene lo stesso risultato.

Ecco alcuni esempi:

Esistono anche gli attivatori di notifiche se vuoi mostrare una notifica in un momento specifico.

Animazione

L'animazione è un elemento visivo, quindi non dovrebbe utilizzare il tempo della CPU quando la pagina è nascosta.

requestAnimationFrame è molto più efficace nella pianificazione del lavoro di animazione rispetto ai timer JavaScript. Si sincronizza con la frequenza di aggiornamento del dispositivo, garantendoti un solo callback per frame visualizzabile e il tempo massimo per costruirlo. Inoltre, requestAnimationFrame attenderà che la pagina sia visibile, quindi non utilizza la CPU quando la pagina è nascosta.

Se puoi dichiarare l'intera animazione in anticipo, ti consigliamo di utilizzare le animazioni CSS o l'API di animazioni web. Queste hanno gli stessi vantaggi di requestAnimationFrame, ma il browser può eseguire ottimizzazioni aggiuntive come il compositing automatico e sono in genere più facili da usare.

Se l'animazione ha una frequenza frame bassa (ad esempio un cursore lampeggiante), al momento i timer sono ancora la migliore opzione, ma puoi combinarli con requestAnimationFrame per ottenere il meglio da entrambe le opzioni:

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);
}

Utilizzo:

const controller = new AbortController();

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

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

Test

Questa modifica verrà attivata per tutti gli utenti di Chrome in Chrome 88 (gennaio 2021). Al momento è attiva per il 50% degli utenti di Chrome Beta, Dev e Canary. Se vuoi provarlo, utilizza questo flag della riga di comando quando avvii Chrome Beta, Dev o Canary:

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

L'argomento grace_period_seconds/10 attiva una limitazione intensa dopo 10 secondi dalla visualizzazione della pagina, anziché dopo 5 minuti, rendendo più facile vedere l'impatto della limitazione.

Il futuro

Poiché i timer sono una fonte di utilizzo eccessivo della CPU, continueremo a esaminare i modi in cui possiamo ridurli senza interrompere i contenuti web e le API che possiamo aggiungere/modificare per soddisfare i casi d'uso. Personalmente, vorrei eliminare la necessità di animationInterval a favore di callback di animazione a bassa frequenza efficienti. Per qualsiasi domanda, contattaci su Twitter.

Foto di intestazione di Heather Zabriskie su Unsplash.