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 unRTCDataChannel
"aperto" o unMediaStreamTrack
"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:
- Se devi sapere quando un elemento entra nell'area visibile, utilizza
IntersectionObserver
. - Se devi sapere quando un elemento cambia dimensione, utilizza
ResizeObserver
. - Se devi sapere quando il DOM cambia, utilizza
MutationObserver
o, eventualmente, i richiami del ciclo di vita degli elementi personalizzati. - Anziché eseguire il polling di un server, valuta la possibilità di utilizzare socket web, eventi inviati dal server, messaggi push o stream di recupero.
- Se devi reagire alle modifiche della fase audio/video, utilizza eventi come
timeupdate
eended
oppurerequestVideoFrameCallback
se devi fare qualcosa con ogni frame.
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.