Supporto dei browser
- 68
- 79
- x
- x
I browser moderni a volte sospendono le pagine o le ignorano completamente quando le risorse di sistema sono limitate. In futuro, i browser preferiscono 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. Date un'occhiata all'API per vedere se dovreste implementare queste funzionalità nella vostra applicazione.
Contesto
Il ciclo di vita delle applicazioni è un modo fondamentale in cui i sistemi operativi moderni gestiscono le risorse. Su Android, iOS e sulle versioni recenti di Windows, le app possono essere avviate e arrestate in qualsiasi momento dal sistema operativo. Questo consente a queste piattaforme di ottimizzare e riallocare le risorse laddove 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 sovraccaricate, causando un'esperienza negativa per l'utente finale.
Anche se la piattaforma web ospita 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 (e in generale sia più consapevole delle risorse su tutte le piattaforme), i browser hanno bisogno di un modo per recuperare e riassegnare in modo proattivo le risorse di sistema.
Infatti, oggi i browser adottano già misure attive per risparmiare risorse per le pagine nelle schede in background e molti browser (in particolare Chrome) vorrebbero farlo molto di più, per ridurre il consumo complessivo di risorse.
Il problema è che gli sviluppatori non hanno modo di prepararsi per questi tipi di interventi avviati dal sistema o persino di sapere che hanno luogo. Ciò significa che i browser devono essere conservativi o rischiare di compromettere le pagine web.
L'API Page Lifecycle prova a risolvere questo problema:
- Introduzione e standardizzazione del concetto di stati del ciclo di vita sul web.
- La definizione di nuovi stati avviati dal sistema che consentono ai browser di limitare le risorse che possono essere utilizzate da schede nascoste o non attive.
- Creazione di nuove API ed eventi che consentano agli sviluppatori web di rispondere alle transizioni da e verso questi nuovi stati avviati dal sistema.
Questa soluzione offre agli sviluppatori web la prevedibilità di cui hanno bisogno per creare applicazioni resilienti agli interventi di sistema e consente ai browser di ottimizzare in modo più aggressivo le risorse di sistema, a beneficio di tutti gli utenti web.
Il resto di questo post presenterà le nuove funzionalità del ciclo di vita delle pagine ed esplorerà la loro relazione con tutti gli stati e gli eventi esistenti delle piattaforme web. Fornirà inoltre consigli e best practice per i tipi di attività che gli sviluppatori dovrebbero (e non devono) svolgere in ogni stato.
Panoramica degli stati e degli eventi del ciclo di vita delle pagine
Tutti gli stati del ciclo di vita delle pagine sono discreti e si escludono a vicenda, ovvero una pagina può trovarsi in uno solo stato alla volta. Inoltre, la maggior parte delle modifiche allo stato del ciclo di vita di una pagina è generalmente osservabile tramite eventi DOM (consulta i suggerimenti per gli sviluppatori per ogni stato per le eccezioni).
Forse il modo più semplice per spiegare gli stati del ciclo di vita delle pagine, così come gli eventi che indicano le transizioni tra loro, è utilizzare un diagramma:
Stati
La tabella seguente illustra in dettaglio ogni stato. Elenca anche i possibili stati che possono verificarsi prima e dopo, nonché gli eventi che gli sviluppatori possono utilizzare per osservare i cambiamenti.
Stato | Descrizione |
---|---|
Attivo |
Una pagina è in stato attivo se è visibile e ha lo stato attivo per l'input.
Possibili stati precedenti: |
Passivo |
Una pagina è in stato passivo se è visibile e non è stata impostata per l'input.
Possibili stati precedenti:
Possibili stati successivi: |
Nascosto |
Una pagina è nello stato nascosto se non è visibile (e non è stata bloccata, eliminata o terminata).
Possibili stati precedenti:
Possibili stati successivi: |
Gelate |
Quando lo stato è bloccato, il browser sospende l'esecuzione delle
attività congelabili nelle
code di attività della pagina finché la pagina non viene sbloccata. Ciò significa che elementi come
i timer JavaScript e i callback di recupero non vengono eseguiti. Le attività già in esecuzione possono essere terminate (soprattutto il callback
I browser bloccano le pagine in modo da preservare l'utilizzo di CPU, batteria e dati. Lo fanno anche per consentire navigazioni back-forward più veloci, evitando di dover ricaricare l'intera pagina.
Possibili stati precedenti:
Possibili stati successivi: |
Terminata |
Una pagina è nello stato terminata dopo che è iniziato l'unload e l'eliminazione dalla memoria del browser. Nessuna nuova attività può iniziare in questo stato e le attività in corso potrebbero essere interrotte se vengono eseguite troppo a lungo.
Possibili stati precedenti:
Possibili stati successivi: |
Ignorati |
Una pagina è nello stato eliminato quando viene eseguito l'unload dal browser per risparmiare risorse. In questo stato non è possibile eseguire attività, callback di eventi o JavaScript di qualsiasi tipo, dato che gli scarti di solito avvengono in base a vincoli delle risorse, dove non è possibile avviare nuovi processi. Nello stato esclusa, la scheda stessa (inclusi il titolo e la favicon) è solitamente visibile all'utente anche se la pagina è stata rimossa.
Possibili stati precedenti:
Possibili stati successivi: |
Eventi
I browser inviano molti eventi, ma solo una piccola parte segnala un possibile cambiamento nello stato del ciclo di vita della pagina. La tabella seguente illustra tutti gli eventi relativi al ciclo di vita ed elenca gli stati da e verso cui possono effettuare la transizione.
Nome | Dettagli |
---|---|
focus
|
È stato attivato un elemento DOM.
Nota: un evento
Possibili stati precedenti:
Possibili stati attuali: |
blur
|
Un elemento DOM ha perso lo stato attivo.
Nota: un evento
Possibili stati precedenti:
Possibili stati attuali: |
visibilitychange
|
Il valore
|
freeze
*
|
La pagina è appena stata bloccata. Le attività congelabili nelle code di attività della pagina non verranno avviate.
Possibili stati precedenti:
Possibili stati attuali: |
resume
*
|
Il browser ha ripristinato una pagina bloccata.
Possibili stati precedenti:
Possibili stati attuali: |
pageshow
|
È in corso il trasferimento di una voce della cronologia della sessione. Potrebbe trattarsi di un caricamento di pagina completamente nuovo o di una pagina acquisita dalla cache back/forward. Se la pagina è stata acquisita dalla cache back-forward, la proprietà
Possibili stati precedenti: |
pagehide
|
È in corso il trasferimento di una voce della cronologia della sessione. Se l'utente passa a un'altra pagina e il browser è in grado di aggiungere la pagina corrente alla cache back/forward per riutilizzarla in un secondo momento, la proprietà
Possibili stati precedenti:
Possibili stati attuali: |
beforeunload
|
Stai per eseguire l'unload della finestra, del documento e delle relative risorse. A questo punto il documento è ancora visibile e l'evento può essere ancora annullato.
Importante: l'evento
Possibili stati precedenti:
Possibili stati attuali: |
unload
|
È in corso l'unload della pagina.
Avviso: l'utilizzo dell'evento
Possibili stati precedenti:
Possibili stati attuali: |
* 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: bloccato e eliminato. Come accennato in precedenza, già oggi i browser si bloccano ed eliminano le schede nascoste (a loro discrezione), ma gli sviluppatori non hanno modo di sapere quando questo accade.
In Chrome 68, ora gli sviluppatori possono 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 per computer (per questo problema è in corso il monitoraggio del supporto di Android). Per determinare se una pagina è stata eliminata in una scheda nascosta, puoi controllarne il valore al momento del caricamento della pagina (nota:
le pagine eliminate devono essere ricaricate per poter essere utilizzate di nuovo).
if (document.wasDiscarded) {
// Page was previously discarded by the browser while in a hidden tab.
}
Per consigli sulle cose importanti da fare negli eventi freeze
e resume
, nonché su come gestire e preparare le pagine che verranno eliminate, consulta i consigli per gli sviluppatori per ogni stato.
Le prossime sezioni offrono una panoramica di come queste nuove funzionalità si adattano agli stati e agli eventi delle piattaforme web esistenti.
Come osservare gli stati del ciclo di vita delle pagine nel codice
Nello stato attivo, passivo e nascosto, è possibile eseguire il codice JavaScript che determina l'attuale stato del ciclo di vita della pagina dalle API delle piattaforme web esistenti.
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
Gli stati bloccato e terminato, invece, possono essere rilevati solo nel rispettivo listener di eventi (freeze
e pagehide
) mentre lo stato cambia.
Come osservare i cambiamenti di stato
Basandoti sulla funzione getState()
definita in precedenza, puoi osservare tutte le modifiche dello stato del ciclo di vita della pagina con il codice seguente.
// 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 ha tre funzioni:
- Imposta lo stato iniziale utilizzando la funzione
getState()
. - Definisce una funzione che accetta uno stato successivo e, in caso di modifica, registra le modifiche dello stato nella console.
- Aggiunge listener di
acquisizione
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 superano
{capture: true}
.
Ecco alcuni dei motivi:
- Non tutti gli eventi del ciclo di vita delle pagine hanno lo stesso target.
pagehide
epageshow
vengono attivati suwindow
;visibilitychange
,freeze
eresume
vengono attivati sudocument
, mentrefocus
eblur
vengono attivati sui rispettivi elementi DOM. - La maggior parte di questi eventi non mostra le bolle, il che significa che è impossibile aggiungere listener di eventi non catturanti a un elemento predecessore comune e osservarli tutti.
- La fase di acquisizione viene eseguita prima delle fasi di destinazione o bubble, quindi l'aggiunta di ascoltatori in questa fase garantisce l'esecuzione prima che un altro codice possa annullarle.
Consigli per gli sviluppatori per ogni stato
Per gli sviluppatori è importante comprendere gli stati del ciclo di vita delle pagine e saper osservarli nel codice, perché il tipo di lavoro che dovresti (e non dovresti) 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. Sebbene questo esempio sia piuttosto ovvio, ci sono altri consigli non così ovvi che vale la pena enunciare.
Stato | Consigli per gli sviluppatori |
---|---|
Active |
Lo stato attivo è il momento più critico per l'utente, quindi il momento più importante affinché la pagina sia adattabile all'input dell'utente. Qualsiasi operazione non UI che potrebbe bloccare il thread principale deve ridurre la priorità a periodi di inattività o ricaricarla a 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, è un buon momento per mantenere 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 a hidden è spesso anche l'ultimo cambiamento di stato osservabile in modo affidabile dagli sviluppatori (questo vale in particolare sui dispositivi mobili, in quanto gli utenti possono chiudere le schede o l'app browser stessa e gli eventi Ciò significa che dovresti considerare lo stato nascosto come la probabile fine della sessione dell'utente. In altre parole, mantieni eventuali stati dell'applicazione non salvati e invia eventuali dati di analisi non inviati. Devi anche interrompere gli aggiornamenti dell'interfaccia utente (dal momento che non saranno visibili all'utente) e interrompere tutte le attività che un utente non vorrebbe eseguire in background. |
|
Frozen |
Nello stato bloccato, le attività bloccabili nelle code di attività vengono sospese fino a quando la pagina non viene sbloccata, il che potrebbe non verificarsi mai (ad esempio se la pagina viene eliminata). Ciò significa che quando la pagina passa da nascosta a bloccata, è essenziale interrompere eventuali timer o eliminare le connessioni che, se bloccate, potrebbero influire su altre schede aperte nella stessa origine o sulla capacità del browser di inserire la pagina nella cache back-forward. In particolare, è importante:
Devi inoltre mantenere qualsiasi stato di visualizzazione dinamica (ad esempio la posizione di scorrimento in una visualizzazione a elenco infinita) su
Se la pagina passa da bloccata a nascosta, puoi riaprire le connessioni chiuse o riavviare qualsiasi polling interrotto quando la pagina era inizialmente bloccata. |
Terminated |
In genere non è necessario alcun intervento da parte tua quando una pagina passa allo stato terminata. Poiché le pagine sottoposte a unload in seguito a un'azione dell'utente passano sempre allo stato nascosto prima di entrare nello stato terminata, lo stato nascosto è quello in cui deve essere eseguita la logica di fine sessione (ad es. stato dell'applicazione permanente e reporting ad analisi). Inoltre, come accennato nei consigli per lo stato nascosto, è molto importante che gli sviluppatori comprendano che la transizione allo stato terminato non può essere rilevata in modo affidabile in molti casi (in particolare sui dispositivi mobili), quindi è probabile che gli sviluppatori che dipendono da eventi di terminazione (ad es. |
Discarded |
Lo stato eliminato non è osservabile dagli sviluppatori nel momento in cui viene eliminata una pagina. Questo perché le pagine vengono generalmente ignorate in base ai vincoli delle risorse e nella maggior parte dei casi non è possibile sbloccare una pagina solo per consentire l'esecuzione dello script in risposta a un evento di eliminazione. Di conseguenza, devi prepararti alla possibilità di un'eliminazione nella modifica da hidden a bloccata, quindi puoi reagire al ripristino di una pagina eliminata al momento del caricamento della pagina selezionando |
Ancora una volta, poiché l'affidabilità e l'ordine degli eventi del ciclo di vita non sono implementati in modo coerente in tutti i browser, il modo più semplice per seguire il consiglio nella tabella è utilizzare PageLifecycle.js.
API con ciclo di vita legacy da evitare
Se possibile, evita i seguenti eventi.
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 dati di analisi, ma farlo
è estremamente inaffidabile, soprattutto sui dispositivi mobili. L'evento unload
non viene attivato in molte situazioni di unload tipiche, ad esempio la chiusura di una scheda dal selettore di schede sui dispositivi mobili o la chiusura dell'app del browser dal selettore di app.
Per questo motivo, è sempre meglio fare affidamento sull'evento visibilitychange
per determinare quando termina una sessione e considerare lo stato nascosto come l'ultimo momento attendibile 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
in avanti e indietro.
In tutti i browser moderni, consigliamo di utilizzare sempre l'evento pagehide
per rilevare i possibili unload delle pagine, ovvero lo stato terminato, anziché l'evento unload
. Se
devi supportare Internet Explorer 10 e versioni precedenti, dovresti 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
ha un problema simile all'evento unload
, in quanto storicamente la presenza di un evento beforeunload
potrebbe impedire alle pagine di essere idonee per la cache back-forward. I browser moderni
non hanno questa restrizione. Anche se alcuni browser, per precauzione, non attivano l'evento beforeunload
quando si cerca di inserire una pagina nella cache back-forward, il che significa che l'evento non è affidabile come indicatore di fine sessione.
Inoltre, alcuni browser (tra cui Chrome) richiedono un'interazione dell'utente sulla pagina prima di consentire l'attivazione dell'evento beforeunload
, il che influisce ulteriormente sulla sua affidabilità.
Una differenza tra beforeunload
e unload
è che esistono
utilizzi legittimi di beforeunload
. Ad esempio, se vuoi avvisare l'utente del fatto che ha modifiche non salvate, perderà se continua a eseguire l'unload della pagina.
Poiché esistono validi motivi per utilizzare beforeunload
, si consiglia di
aggiungere listener beforeunload
solo quando un utente dispone di modifiche non salvate e poi
rimuoverli immediatamente dopo il salvataggio.
In altre parole, non eseguire questa operazione (dato che aggiunge un listener beforeunload
senza condizioni):
addEventListener('beforeunload', (event) => {
// A function that returns `true` if the page has unsaved changes.
if (pageHasUnsavedChanges()) {
event.preventDefault();
// Legacy support for older browsers.
return (event.returnValue = true);
}
});
Esegui questa operazione (dal momento che aggiunge il listener beforeunload
solo quando è necessario e lo rimuove quando non è necessario):
const beforeUnloadListener = (event) => {
event.preventDefault();
// Legacy support for older browsers.
return (event.returnValue = true);
};
// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
addEventListener('beforeunload', beforeUnloadListener);
});
// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
removeEventListener('beforeunload', beforeUnloadListener);
});
Domande frequenti
Perché non è presente lo stato di "caricamento"?
L'API Page Lifecycle definisce gli stati discreti e si escludono a vicenda. Poiché una pagina può essere caricata nello stato attivo, passivo o nascosto e può cambiare stato (o persino essere terminata) prima che termini il caricamento, uno stato di caricamento separato non ha senso all'interno di questo paradigma.
La mia pagina funziona importante quando è nascosta. Come faccio a evitare che venga bloccata o eliminata?
Esistono molti motivi legittimi per cui le pagine web non devono essere bloccate mentre sono in esecuzione nello stato nascosto. L'esempio più ovvio è un'app che riproduce musica.
Esistono anche situazioni in cui sarebbe rischioso che Chrome elimini una pagina, ad esempio se contiene un modulo con input utente non inviato o se ha un gestore beforeunload
che avvisa quando la pagina viene scaricata.
Per il momento, Chrome è conservativo quando elimini le pagine e lo fa solo quando è sicuro che non influisca sugli utenti. Ad esempio, le pagine in cui è stato osservato che, in stato nascosto, eseguono una delle seguenti operazioni:
- Riproduzione dell'audio in corso...
- Utilizzo di WebRTC
- Aggiornamento del titolo o della favicon della tabella
- Visualizzazione degli avvisi
- Invio di notifiche push
Per le funzionalità dell'elenco attuali utilizzate per determinare se una scheda può essere bloccata in sicurezza o eliminata, consulta la pagina Euristica per il blocco ed eliminazione di Chrome.
Che cos'è la cache back-forward?
Cache back/forward è un termine utilizzato per descrivere un'ottimizzazione della navigazione implementata da alcuni browser che velocizza 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 ripristinata rapidamente nel caso in cui l'utente torni indietro o avanti utilizzando i pulsanti Indietro o Avanti. Ricorda che l'aggiunta di un gestore di eventi unload
impedisce che questa ottimizzazione sia possibile.
A tutti gli effetti, questo blocco funziona come previsto dai browser per risparmiare CPU/batteria; per questo motivo è considerato parte dello stato del ciclo di vita bloccato.
Se non riesco a eseguire API asincrone negli stati bloccati o terminati, come posso salvare i dati in IndexedDB?
Nello stato bloccato e terminato, le attività bloccabili nelle code di attività di una pagina vengono sospese, il che significa che le API asincrone e basate su callback, come IndexedDB, non possono essere utilizzate in modo affidabile.
In futuro, aggiungeremo un metodo commit()
agli oggetti IDBTransaction
per offrire agli sviluppatori un modo per eseguire transazioni di sola scrittura che non richiedono callback. In altre parole, se lo sviluppatore sta solo scrivendo dati in IndexedDB e non sta eseguendo una transazione complessa composta da letture e scritture, il metodo commit()
potrà terminare prima che le code di attività vengano sospese (supponendo che il database IndexedDB sia già aperto).
Tuttavia, per il codice che deve funzionare oggi stesso, gli sviluppatori hanno due opzioni:
- Utilizza l'archiviazione della sessione: l'archiviazione della sessione è sincrona e persistente in tutte le eliminazioni di pagine.
- Utilizza IndexedDB dal tuo service worker: un service worker può archiviare dati in IndexedDB dopo che la pagina è stata terminata o eliminata. Nel listener di eventi
freeze
opagehide
puoi inviare dati al service worker tramitepostMessage()
e il service worker può gestire il salvataggio dei dati.
Test dell'app negli stati bloccati ed eliminati
Per verificare il comportamento dell'app negli stati bloccati ed eliminati, puoi visitare la pagina chrome://discards
per bloccare o eliminare le 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 degli utenti devono creare le app tenendo presente gli stati del ciclo di vita delle pagine. È 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 del ciclo di vita delle pagine, più sicuro sarà il blocco e l'eliminazione delle pagine che non vengono utilizzate dai browser. Ciò significa che i browser consumano meno memoria, CPU, batteria e risorse di rete, il che è positivo per gli utenti.