Browser Support
I browser moderni a volte sospendono le pagine o le eliminano completamente quando le risorse di sistema sono limitate. In futuro, i browser vogliono farlo in modo proattivo, in modo da consumare meno energia e memoria. L'API Page Lifecycle fornisce hook del ciclo di vita in modo che le pagine possano gestire in sicurezza questi interventi del browser senza influire sull'esperienza utente. Dai un'occhiata all'API per vedere se devi implementare queste funzionalità nella tua applicazione.
Sfondo
Il ciclo di vita delle applicazioni è un modo fondamentale in cui i sistemi operativi moderni gestiscono le risorse. Su Android, iOS e le versioni recenti di Windows, le app possono essere avviate e interrotte in qualsiasi momento dal sistema operativo. In questo modo, queste piattaforme possono semplificare e riassegnare le risorse dove sono più utili per l'utente.
Sul web, storicamente non esiste un ciclo di vita di questo tipo e le app possono essere mantenute attive a tempo indeterminato. Con un numero elevato di pagine web in esecuzione, le risorse di sistema critiche come memoria, CPU, batteria e rete possono essere sovrascritte, il che comporta un'esperienza utente finale negativa.
Sebbene la piattaforma web abbia da tempo eventi correlati agli stati del ciclo di vita, come load, unload e visibilitychange, questi eventi consentono agli sviluppatori di rispondere solo alle modifiche dello stato del ciclo di vita avviate dall'utente. Affinché il web funzioni
in modo affidabile su dispositivi a basso consumo energetico (e sia più attento alle risorse in generale su
tutte le piattaforme), i browser hanno bisogno di un modo per recuperare e riallocare in modo proattivo le risorse di sistema.
Infatti, i browser di oggi adottano già misure attive per risparmiare risorse per le pagine nelle schede in background e molti browser (in particolare Chrome) vorrebbero fare molto di più per ridurre l'impronta complessiva delle risorse.
Il problema è che gli sviluppatori non hanno modo di prepararsi a questi tipi di interventi avviati dal sistema o addirittura di sapere che si stanno verificando. Ciò significa che i browser devono essere conservativi o rischiano di danneggiare le pagine web.
L'API Page Lifecycle tenta di risolvere questo problema:
- Introduzione e standardizzazione del concetto di stati del ciclo di vita sul web.
- Definizione di nuovi stati avviati dal sistema che consentono ai browser di limitare le risorse che possono essere utilizzate da schede nascoste o inattive.
- Creazione di nuove API ed eventi che consentono agli sviluppatori web di rispondere alle transizioni verso e da questi nuovi stati avviati dal sistema.
Questa soluzione offre la prevedibilità di cui gli sviluppatori web hanno bisogno per creare applicazioni resilienti agli interventi del sistema e consente ai browser di ottimizzare in modo più aggressivo le risorse di sistema, a vantaggio di tutti gli utenti web.
Il resto di questo post introdurrà le nuove funzionalità del ciclo di vita della pagina e analizzerà la loro relazione con tutti gli stati e gli eventi della piattaforma web esistenti. Fornirà inoltre consigli e best practice sui tipi di lavoro che gli sviluppatori devono (e non devono) svolgere in ogni stato.
Panoramica degli stati e degli eventi del ciclo di vita della pagina
Tutti gli stati del ciclo di vita della pagina sono discreti e reciprocamente esclusivi, il che significa che una pagina può trovarsi in un solo stato alla volta. La maggior parte delle modifiche allo stato del ciclo di vita di una pagina sono generalmente osservabili tramite gli eventi DOM (per le eccezioni, consulta i consigli per gli sviluppatori per ogni stato).
Forse il modo più semplice per spiegare gli stati del ciclo di vita della pagina, nonché gli eventi che segnalano le transizioni tra questi stati, è con un diagramma:
Stati
La tabella seguente spiega in dettaglio ogni stato. Elenca anche i possibili stati che possono precedere e seguire, nonché gli eventi che gli sviluppatori possono utilizzare per osservare le modifiche.
| Stato | Descrizione |
|---|---|
| Attivi |
Una pagina è nello stato attivo se è visibile e ha il focus di input.
Stati precedenti possibili: |
| Passivo |
Una pagina è in stato passivo se è visibile e non ha il focus di input.
Stati precedenti possibili:
Possibili stati successivi: |
| Nascosto |
Una pagina è nello stato nascosta se non è visibile (e non è stata bloccata, eliminata o terminata).
Possibili stati precedenti:
Possibili stati successivi: |
| Congelato |
Nello stato congelato, il browser sospende l'esecuzione delle
attività congelabili
nelle code di attività della pagina
fino a quando la pagina non viene scongelata. Ciò significa che elementi come
i timer JavaScript e i callback di recupero non vengono eseguiti. Le attività
già in esecuzione possono essere completate (soprattutto il callback
I browser bloccano le pagine per preservare l'utilizzo di CPU, batteria e dati; lo fanno anche per consentire una navigazione più rapida avanti/indietro, evitando la necessità di ricaricare completamente la pagina.
Possibili stati precedenti:
Possibili stati successivi: |
| Risolto |
Una pagina si trova nello stato terminato una volta che il browser ha iniziato a scaricarla e a cancellarla dalla memoria. No nuove attività possono essere avviate in questo stato e le attività in corso potrebbero essere interrotte se vengono eseguite troppo a lungo.
Possibili stati precedenti:
Possibili stati successivi: |
| Ignorato |
Una pagina è nello stato ignorata quando viene scaricata dal browser per risparmiare risorse. In questo stato non possono essere eseguiti attività, callback di eventi o JavaScript di alcun tipo, poiché gli scarti in genere si verificano in presenza di vincoli di risorse, dove l'avvio di nuovi processi è impossibile. Nello stato Eliminata, la scheda stessa (inclusi il titolo e la favicon) è in genere visibile all'utente anche se la pagina non è più presente.
Possibili stati precedenti:
Possibili stati successivi: |
Eventi
I browser inviano molti eventi, ma solo una piccola parte di questi segnala un possibile cambiamento nello stato del ciclo di vita della pagina. La tabella seguente descrive tutti gli eventi relativi al ciclo di vita ed elenca gli stati da cui e verso cui possono passare.
| Nome | Dettagli |
|---|---|
focus
|
Un elemento DOM ha ricevuto lo stato attivo.
Nota:un evento
Stati precedenti possibili:
Stati attuali possibili: |
blur
|
Un elemento DOM ha perso lo stato attivo.
Nota:un evento
Stati precedenti possibili:
Stati attuali possibili: |
visibilitychange
|
Il valore
|
freeze
*
|
La pagina è stata appena bloccata. Nessuna attività congelabile nelle code di attività della pagina verrà avviata.
Stati precedenti possibili:
Possibili stati attuali: |
resume
*
|
Il browser ha ripristinato una pagina bloccata.
Stati precedenti possibili:
Possibili stati attuali: |
pageshow
|
Si sta passando a una voce della cronologia delle sessioni. Potrebbe trattarsi di un caricamento di una pagina nuova o di una pagina presa dalla
cache back-forward. Se la pagina
è stata presa dalla cache back-forward, la proprietà
Possibili stati precedenti: |
pagehide
|
Una voce della cronologia delle sessioni viene attraversata. Se l'utente passa a un'altra pagina e il browser è in grado di aggiungere
la pagina corrente alla cache
indietro/avanti per essere riutilizzata in un secondo momento, la proprietà
Stati precedenti possibili:
Possibili stati attuali: |
beforeunload
|
La finestra, il documento e le relative risorse stanno per essere scaricati. Il documento è ancora visibile e l'evento è ancora annullabile a questo punto.
Importante:l'evento
Stati precedenti possibili:
Stati attuali possibili: |
unload
|
La pagina viene scaricata.
Avviso:l'utilizzo dell'evento
Stati precedenti possibili:
Stati attuali possibili: |
* Indica un nuovo evento definito dall'API Page Lifecycle
Nuove funzionalità aggiunte in Chrome 68
Il grafico precedente mostra due stati avviati dal sistema anziché dall'utente: frozen e discarded. Come accennato in precedenza, i browser di oggi a volte si bloccano e chiudono le schede nascoste (a loro discrezione), ma gli sviluppatori non hanno modo di sapere quando ciò accade.
In Chrome 68, gli sviluppatori possono ora osservare quando una scheda nascosta viene bloccata e
sbloccata ascoltando gli eventi freeze
e resume su document.
document.addEventListener('freeze', (event) => {
// The page is now frozen.
});
document.addEventListener('resume', (event) => {
// The page has been unfrozen.
});
A partire da Chrome 68, l'oggetto document ora include una proprietà
wasDiscarded
su Chrome desktop (il supporto di Android è in fase di monitoraggio in questo problema). Per determinare se una pagina è stata eliminata mentre si trovava in una scheda nascosta, puoi esaminare il valore di questa proprietà al momento del caricamento della pagina (nota: le pagine eliminate devono essere ricaricate per essere utilizzate di nuovo).
if (document.wasDiscarded) {
// Page was previously discarded by the browser while in a hidden tab.
}
Per consigli su cosa è importante fare negli eventi freeze e resume, nonché su come gestire e prepararsi per l'eliminazione delle pagine, consulta i consigli per gli sviluppatori per ogni stato.
Le sezioni successive offrono una panoramica di come queste nuove funzionalità si inseriscono negli stati ed eventi esistenti della piattaforma web.
Come osservare gli stati del ciclo di vita della pagina nel codice
Negli stati attivo, passivo e nascosto, è possibile eseguire codice JavaScript che determina lo stato del ciclo di vita della pagina corrente dalle API della piattaforma web esistenti.
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
Gli stati congelato e terminato, d'altra parte, possono essere rilevati solo nei rispettivi listener di eventi (freeze e pagehide) durante la modifica dello stato.
Come osservare le modifiche dello stato
Basandosi sulla funzione getState() definita in precedenza, puoi osservare tutte le modifiche dello stato del ciclo di vita della pagina con il seguente codice.
// Stores the initial state using the `getState()` function (defined above).
let state = getState();
// Accepts a next state and, if there's been a state change, logs the
// change to the console. It also updates the `state` value defined above.
const logStateChange = (nextState) => {
const prevState = state;
if (nextState !== prevState) {
console.log(`State change: ${prevState} >>> ${nextState}`);
state = nextState;
}
};
// Options used for all event listeners.
const opts = {capture: true};
// These lifecycle events can all use the same listener to observe state
// changes (they call the `getState()` function to determine the next state).
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach((type) => {
window.addEventListener(type, () => logStateChange(getState()), opts);
});
// The next two listeners, on the other hand, can determine the next
// state from the event itself.
window.addEventListener('freeze', () => {
// In the freeze event, the next state is always frozen.
logStateChange('frozen');
}, opts);
window.addEventListener('pagehide', (event) => {
// If the event's persisted property is `true` the page is about
// to enter the back/forward cache, which is also in the frozen state.
// If the event's persisted property is not `true` the page is
// about to be unloaded.
logStateChange(event.persisted ? 'frozen' : 'terminated');
}, opts);
Questo codice esegue tre operazioni:
- Imposta lo stato iniziale utilizzando la funzione
getState(). - Definisce una funzione che accetta uno stato successivo e, se si verifica una modifica, registra le modifiche dello stato nella console.
- Aggiunge
l'acquisizione
di listener di eventi per tutti gli eventi del ciclo di vita necessari, che a loro volta chiamano
logStateChange(), passando allo stato successivo.
Una cosa da notare del codice è che tutti i listener di eventi vengono aggiunti
a window e tutti passano
{capture: true}.
Ecco alcuni dei motivi:
- Non tutti gli eventi del ciclo di vita della pagina hanno lo stesso target.
pagehideepageshowvengono attivati suwindow;visibilitychange,freezeeresumevengono attivati sudocument, mentrefocuseblurvengono attivati sui rispettivi elementi DOM. - La maggior parte di questi eventi non si propaga, il che significa che è impossibile aggiungere listener di eventi non di acquisizione a un elemento antenato comune e osservarli tutti.
- La fase di acquisizione viene eseguita prima delle fasi di destinazione o di bolla, quindi l'aggiunta di listener in questa fase contribuisce a garantire che vengano eseguiti prima che altro codice possa annullarli.
Consigli per gli sviluppatori per ogni stato
In qualità di sviluppatori, è importante sia comprendere gli stati del ciclo di vita della pagina sia sapere come osservarli nel codice, perché il tipo di lavoro che devi (e non devi) svolgere dipende in gran parte dallo stato della pagina.
Ad esempio, non ha senso mostrare una notifica temporanea all'utente se la pagina è in stato nascosto. Anche se questo esempio è piuttosto ovvio, ci sono altri consigli meno evidenti che vale la pena elencare.
| Stato | Consigli per gli sviluppatori |
|---|---|
Active |
Lo stato attivo è il momento più critico per l'utente e quindi il momento più importante per la tua pagina per essere reattiva all'input dell'utente. Qualsiasi lavoro non UI che potrebbe bloccare il thread principale deve essere declassato a periodi di inattività o scaricato su un web worker. |
Passive |
Nello stato passivo, l'utente non interagisce con la pagina, ma può comunque vederla. Ciò significa che gli aggiornamenti e le animazioni dell'interfaccia utente devono comunque essere fluidi, ma la tempistica di questi aggiornamenti è meno critica. Quando la pagina passa da attiva a passiva, è il momento giusto per rendere persistente lo stato dell'applicazione non salvato. |
|
Quando la pagina passa da passiva a nascosta, è possibile che l'utente non interagisca di nuovo con la pagina finché non viene ricaricata. La transizione allo stato nascosto è spesso anche l'ultimo cambio di stato
che gli sviluppatori possono osservare in modo affidabile (ciò vale soprattutto per
i dispositivi mobili, in quanto gli utenti possono chiudere le schede o l'app del browser e gli
eventi Ciò significa che devi considerare lo stato nascosto come la probabile fine della sessione dell'utente. In altre parole, mantieni lo stato dell'applicazione non salvato e invia i dati di analisi non inviati. Dovresti anche interrompere gli aggiornamenti dell'interfaccia utente (poiché non verranno visualizzati dall'utente) e qualsiasi attività che un utente non vorrebbe in esecuzione in background. |
|
Frozen |
Nello stato congelato, le attività congelabili nelle code di attività vengono sospese finché la pagina non viene scongelata, il che potrebbe non accadere mai (ad es. se la pagina viene eliminata). Ciò significa che quando la pagina passa da nascosta a congelata, è essenziale interrompere tutti i timer o chiudere tutte le connessioni che, se congelate, potrebbero influire su altre schede aperte nella stessa origine o sulla capacità del browser di inserire la pagina nella cache indietro/avanti. In particolare, è importante che tu:
Devi anche rendere persistente qualsiasi stato di visualizzazione dinamico (ad es. la posizione di scorrimento
in una visualizzazione elenco infinita) in
Se la pagina passa dallo stato congelato a nascosto, puoi riaprire le connessioni chiuse o riavviare il polling interrotto quando la pagina è stata inizialmente congelata. |
Terminated |
In genere, non è necessario intraprendere alcuna azione quando una pagina passa allo stato terminato. Poiché le pagine scaricate a seguito dell'azione dell'utente passano sempre attraverso lo stato hidden prima di entrare nello stato terminated, lo stato hidden è quello in cui deve essere eseguita la logica di fine sessione (ad es. la persistenza dello stato dell'applicazione e la generazione di report per Analytics). Inoltre (come indicato nei consigli per lo stato
nascosto), è molto importante che gli sviluppatori si rendano conto
che la transizione allo stato terminato non può essere rilevata in modo affidabile
in molti casi (soprattutto sui dispositivi mobili), quindi gli sviluppatori che dipendono
da eventi di terminazione (ad es. |
Discarded |
Lo stato eliminato non è osservabile dagli sviluppatori nel momento in cui una pagina viene eliminata. Questo perché le pagine vengono in genere eliminate in base ai limiti delle risorse e riattivarle solo per consentire l'esecuzione dello script in risposta a un evento di eliminazione non è possibile nella maggior parte dei casi. Di conseguenza, devi prepararti alla possibilità di un annullamento
della modifica da nascosta a congelata, quindi puoi
reagire al ripristino di una pagina annullata al momento del caricamento della pagina
controllando |
Ancora una volta, poiché l'affidabilità e l'ordinamento degli eventi del ciclo di vita non sono implementati in modo coerente in tutti i browser, il modo più semplice per seguire i consigli riportati nella tabella è utilizzare PageLifecycle.js.
API per il ciclo di vita legacy da evitare
I seguenti eventi devono essere evitati, se possibile.
L'evento unload
Molti sviluppatori considerano l'evento unload come un callback garantito e lo utilizzano come
indicatore di fine sessione per salvare lo stato e inviare i dati di analisi, ma questa operazione
è estremamente inaffidabile, soprattutto sui dispositivi mobili. L'evento unload non viene attivato in molte situazioni di scaricamento tipiche, tra cui la chiusura di una scheda dal selettore di schede su dispositivo mobile o la chiusura dell'app del browser dal selettore di app.
Per questo motivo, è sempre meglio fare affidamento all'evento
visibilitychange per determinare quando termina una sessione e considerare lo stato nascosto come
l'ultimo momento affidabile per salvare i dati dell'app e dell'utente.
Inoltre, la semplice presenza di un gestore di eventi unload registrato (tramite
onunload o addEventListener()) può impedire ai browser di inserire le pagine nella
cache back-forward per caricamenti più rapidi
avanti e indietro.
In tutti i browser moderni, ti consigliamo di utilizzare sempre l'evento
pagehide per rilevare possibili scaricamenti di pagina (ovvero lo stato
terminated) anziché l'evento unload. Se devi supportare Internet Explorer 10 e versioni precedenti, devi rilevare l'evento pagehide e utilizzare unload solo se il browser non supporta pagehide:
const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';
window.addEventListener(terminationEvent, (event) => {
// Note: if the browser is able to cache the page, `event.persisted`
// is `true`, and the state is frozen rather than terminated.
});
L'evento beforeunload
L'evento beforeunload presenta un problema simile all'evento unload, in quanto
storicamente la presenza di un evento beforeunload poteva impedire alle pagine di
essere idonee per la cache back-forward. I browser moderni
non hanno questa limitazione. Anche se alcuni browser, per precauzione, non attivano
l'evento beforeunload quando tentano di inserire una pagina nella cache
indietro/avanti, il che significa che l'evento non è affidabile come indicatore di fine sessione.
Inoltre, alcuni browser (incluso Chrome)
richiedono un'interazione dell'utente sulla pagina prima di consentire l'attivazione dell'evento beforeunload, influenzandone ulteriormente l'affidabilità.
Una differenza tra beforeunload e unload è che esistono
usi legittimi di beforeunload. Ad esempio, quando vuoi avvisare l'utente
che ha modifiche non salvate che perderà se continua a scaricare la pagina.
Poiché esistono motivi validi per utilizzare beforeunload, ti consigliamo di
aggiungere solo listener beforeunload quando un utente ha modifiche non salvate e di
rimuoverli immediatamente dopo il salvataggio.
In altre parole, non fare questo (perché aggiunge un listener beforeunload
in modo incondizionato):
addEventListener('beforeunload', (event) => {
// A function that returns `true` if the page has unsaved changes.
if (pageHasUnsavedChanges()) {
event.preventDefault();
// Legacy support for older browsers.
event.returnValue = true;
}
});
Invece, fai così (in quanto aggiunge il listener beforeunload solo quando è necessario e lo rimuove quando non lo è):
const beforeUnloadListener = (event) => {
event.preventDefault();
// Legacy support for older browsers.
event.returnValue = true;
};
// A function that adds a `beforeunload` listener if there are unsaved changes.
onPageHasUnsavedChanges(() => {
addEventListener('beforeunload', beforeUnloadListener);
});
// A function that removes the `beforeunload` listener when the page's unsaved
// changes are resolved.
onAllChangesSaved(() => {
removeEventListener('beforeunload', beforeUnloadListener);
});
Domande frequenti
Perché non è presente uno stato "Caricamento"?
L'API Page Lifecycle definisce stati discreti e reciprocamente esclusivi. Poiché una pagina può essere caricata nello stato attivo, passivo o nascosto e poiché può cambiare stato o addirittura essere terminata prima del completamento del caricamento, uno stato di caricamento separato non ha senso in questo paradigma.
La mia pagina svolge un lavoro importante quando è nascosta. Come posso impedire che venga bloccata o eliminata?
Esistono molti motivi legittimi per cui le pagine web non devono essere bloccate durante l'esecuzione nello stato nascosto. L'esempio più ovvio è un'app che riproduce musica.
Esistono anche situazioni in cui sarebbe rischioso per Chrome eliminare una pagina,
ad esempio se contiene un modulo con input utente non inviati o se ha un
gestore beforeunload che avvisa quando la pagina viene scaricata.
Per il momento, Chrome sarà conservativo quando scarta le pagine e lo farà solo quando sarà sicuro che non influirà sugli utenti. Ad esempio, le pagine che hanno eseguito una delle seguenti operazioni in stato nascosto non verranno eliminate, a meno che non si verifichino vincoli estremi delle risorse:
- Riproduzione audio
- Utilizzo di WebRTC
- Aggiornamento del titolo della tabella o della favicon
- Visualizzazione degli avvisi
- Invio di notifiche push
Per l'elenco attuale delle funzionalità utilizzate per determinare se una scheda può essere bloccata o eliminata in modo sicuro, consulta: Euristiche per il blocco e l'eliminazione in Chrome.
Che cos'è la cache back-forward?
La cache back-forward è un termine utilizzato per descrivere un'ottimizzazione della navigazione implementata da alcuni browser che rende più veloce l'utilizzo dei pulsanti Indietro e Avanti.
Quando un utente esce da una pagina, questi browser bloccano una versione della
pagina in modo che possa essere ripresa rapidamente nel caso in cui l'utente torni indietro utilizzando
i pulsanti Indietro o Avanti. Ricorda che l'aggiunta di un unload
gestore eventi impedisce questa ottimizzazione.
A tutti gli effetti, questo blocco è funzionalmente identico a quello eseguito dai browser per risparmiare CPU/batteria; per questo motivo, è considerato parte dello stato del ciclo di vita congelato.
Se non riesco a eseguire API asincrone negli stati bloccato o terminato, come faccio a salvare i dati in IndexedDB?
Negli stati congelato e terminato, le attività congelabili nelle code di attività di una pagina vengono sospese, il che significa che le API asincrone e basate su callback non possono essere utilizzate in modo affidabile.
Sebbene la maggior parte delle API IndexedDB sia basata su callback, il metodo
commit()
dell'interfaccia
IDBTransaction
fornisce un modo per avviare la procedura di commit su una transazione attiva senza attendere l'invio degli eventi dalle richieste in sospeso. In questo modo
viene fornito un modo affidabile per salvare i dati in un database IndexedDB in un listener di eventi freeze o
visibilitychange perché il commit viene eseguito immediatamente
anziché essere messo in coda in un'attività separata.
Testare l'app negli stati congelato e eliminato
Per testare il comportamento dell'app negli stati di blocco e chiusura, puoi visitare
chrome://discards per bloccare o chiudere effettivamente una delle tue
schede aperte.
In questo modo, puoi assicurarti che la pagina gestisca correttamente gli eventi freeze e resume
nonché il flag document.wasDiscarded quando le pagine vengono ricaricate dopo
un'eliminazione.
Riepilogo
Gli sviluppatori che vogliono rispettare le risorse di sistema dei dispositivi dei loro utenti devono creare le loro app tenendo presente gli stati del ciclo di vita della pagina. È fondamentale che le pagine web non consumino risorse di sistema eccessive in situazioni che l'utente non si aspetta.
Più sviluppatori iniziano a implementare le nuove API Page Lifecycle, più sarà sicuro per i browser bloccare e scartare le pagine che non vengono utilizzate. Ciò significa che i browser consumeranno meno memoria, CPU, batteria e risorse di rete, il che è un vantaggio per gli utenti.