Compatibilidade com navegadores
Os navegadores modernos, às vezes, suspendem ou descartam páginas completamente quando os recursos do sistema estão restritos. No futuro, os navegadores vão querer fazer isso de forma proativa, para consumir menos energia e memória. A API Page Lifecycle oferece hooks de ciclo de vida para que suas páginas possam processar com segurança essas intervenções do navegador sem afetar a experiência do usuário. Confira a API para saber se você precisa implementar esses recursos no seu app.
Contexto
O ciclo de vida do aplicativo é uma forma essencial de gerenciar recursos para os sistemas operacionais modernos. No Android, no iOS e em versões recentes do Windows, os apps podem ser iniciados e parados a qualquer momento pelo SO. Isso permite que essas plataformas simplifiquem e realocem os recursos da forma mais benéfica para o usuário.
Historicamente, na Web não existe nada parecido, e os apps podem ficar ativos para sempre. Com um grande número de páginas da Web em execução, recursos críticos do sistema, como memória, CPU, bateria e rede, podem ser consumidos em excesso, provocando uma experiência ruim para o usuário.
Embora a plataforma da Web tenha eventos relacionados aos estados do ciclo de vida
— como load
,
unload
e
visibilitychange
— esses eventos só permitem que os desenvolvedores
respondam a mudanças de estado do ciclo de vida iniciadas pelo usuário. Para que a Web funcione
de maneira confiável em dispositivos de baixa potência (e seja mais consciente dos recursos em geral em
todas as plataformas), os navegadores precisam de uma maneira de recuperar e realocar proativamente os recursos
do sistema.
Na verdade, os navegadores de hoje já tomam medidas ativas para economizar recursos em páginas em guias em segundo plano, e muitos navegadores (especialmente o Chrome) gostariam de fazer muito mais para reduzir o impacto geral dos recursos.
O problema é que os desenvolvedores não têm como se preparar para esses tipos de intervenções iniciadas pelo sistema ou mesmo saber que elas estão acontecendo. Isso significa que os navegadores precisam ser conservadores ou correm o risco de quebrar as páginas da Web.
A API Page Lifecycle tenta resolver esse problema da seguinte forma:
- Introdução e padronização do conceito de estados do ciclo de vida na Web.
- Definição de novos estados iniciados pelo sistema que permitem que os navegadores limitem os recursos que podem ser consumidos por guias ocultas ou inativas.
- Criação de novas APIs e eventos que permitem que desenvolvedores da Web respondam a transições para e a partir desses novos estados iniciados pelo sistema.
Essa solução oferece a previsibilidade que os desenvolvedores da Web precisam para criar aplicativos resilientes a intervenções do sistema e permite que os navegadores otimizem mais agressivamente os recursos do sistema, beneficiando todos os usuários da Web.
No restante desta postagem, vamos apresentar os novos recursos do ciclo de vida da página e mostrar como eles se relacionam a todos os estados e eventos da plataforma da Web. Ele também vai dar recomendações e práticas recomendadas para os tipos de trabalho que os desenvolvedores devem (e não devem) fazer em cada estado.
Visão geral dos estados e eventos do ciclo de vida da página
Todos os estados do ciclo de vida da página são discretos e mutuamente exclusivos, o que significa que uma página só pode estar em um estado por vez. E a maioria das mudanças no estado do ciclo de vida de uma página geralmente é observável por eventos do DOM. Consulte as recomendações para desenvolvedores de cada estado para ver as exceções.
Talvez a maneira mais fácil de explicar os estados do ciclo de vida da página, bem como os eventos que sinalizam transições entre eles, seja com um diagrama:
Estados
A tabela a seguir explica cada estado em detalhes. Ele também lista os possíveis estados que podem ocorrer antes e depois, bem como os eventos que os desenvolvedores podem usar para observar mudanças.
Estado | Descrição |
---|---|
Ativa |
Uma página está no estado ativo se estiver visível e tiver foco de entrada.
Possíveis estados anteriores: |
Passiva |
Uma página está no estado passivo se estiver visível e não tiver foco de entrada.
Possíveis estados anteriores:
Possíveis próximos estados: |
Ocultos |
Uma página está no estado oculto se não está visível (e não foi congelada, descartada ou encerrada).
Possíveis estados anteriores:
Possíveis próximos estados: |
Frozen |
No estado congelado, o navegador suspende a execução de
tarefas
freezáveis nas
filas de tarefas da página até que ela seja descongelada. Isso significa que coisas como
timers de JavaScript e callbacks de busca não são executados. As tarefas que já estão em execução
podem ser concluídas (principalmente o callback
Os navegadores congelam páginas para preservar o uso de CPU/bateria/dados. Eles também fazem isso para permitir navegações para frente/para trás mais rápidas, evitando a necessidade de uma recarga de página completa.
Possíveis estados anteriores:
Possíveis estados seguintes: |
Encerrado |
Uma página está no estado encerrada quando começa a ser descarregada e limpa da memória pelo navegador. Nenhuma nova tarefa pode ser iniciada nesse estado, e as tarefas em andamento podem ser encerradas se durarem muito.
Possíveis estados anteriores:
Possíveis estados seguintes: |
Descartado |
Uma página está no estado descartada quando é descarregada pelo navegador para conservar recursos. Nenhuma tarefa, callback de evento ou JavaScript de qualquer tipo pode ser executado nesse estado, porque descartes normalmente ocorrem com restrições de recursos, em que a inicialização de novos processos é impossível. No estado descartado, a guia (incluindo o título e o ícone da guia) geralmente fica visível para o usuário, mesmo que a página não esteja mais disponível.
Possíveis estados anteriores:
Possíveis estados seguintes: |
Eventos
Os navegadores enviam muitos eventos, mas apenas uma pequena parte deles sinaliza uma possível mudança no estado do ciclo de vida da página. A tabela a seguir descreve todos os eventos relacionados ao ciclo de vida e lista os estados para os quais eles podem fazer a transição.
Nome | Detalhes |
---|---|
focus
|
Um elemento DOM recebeu foco.
Observação:um evento
Possíveis estados anteriores:
Estados atuais possíveis: |
blur
|
Um elemento DOM perdeu o foco.
Observação:um evento
Estados anteriores possíveis:
Estados atuais possíveis: |
visibilitychange
|
O valor de
|
freeze
*
|
A página foi congelada. Qualquer tarefa freezável nas filas de tarefas da página não será iniciada.
Estados anteriores possíveis:
Possíveis estados atuais: |
resume
*
|
O navegador retomou uma página congelada.
Possíveis estados anteriores:
Possíveis estados atuais: |
pageshow
|
Uma entrada do histórico de sessão está sendo acessada. Pode ser um carregamento de página totalmente novo ou uma página tirada do
cache de avanço e retorno. Se a página
foi tirada do cache de avanço e retorno, a propriedade
Possíveis estados anteriores: |
pagehide
|
Uma entrada do histórico de sessão está sendo percorrida. Se o usuário estiver navegando para outra página e o navegador puder adicionar
a página atual ao cache de ida e volta para ser reutilizada mais tarde, a propriedade
Possíveis estados anteriores:
Possíveis estados atuais: |
beforeunload
|
A janela, o documento e os recursos dele estão prestes a ser descarregados. O documento ainda está visível e o evento ainda pode ser cancelado neste ponto.
Importante:o evento
Possíveis estados anteriores:
Estados atuais possíveis: |
unload
|
A página está sendo descarregada.
Aviso:o uso do evento
Estados anteriores possíveis:
Estados atuais possíveis: |
* Indica um novo evento definido pela API Page Lifecycle
Novos recursos adicionados no Chrome 68
O gráfico anterior mostra dois estados iniciados pelo sistema, e não pelo usuário: congelado e descartado. Como mencionado anteriormente, os navegadores de hoje já congelam e descartam acidentalmente guias ocultas, mas os desenvolvedores não têm como saber quando isso está acontecendo.
No Chrome 68, os desenvolvedores agora podem observar quando uma guia oculta é travada e
descongelada detectando os eventos freeze
e resume
em document
.
document.addEventListener('freeze', (event) => {
// The page is now frozen.
});
document.addEventListener('resume', (event) => {
// The page has been unfrozen.
});
No Chrome 68, o objeto document
agora inclui uma propriedade
wasDiscarded
no Chrome para computador (o suporte para Android está sendo rastreado nesta questão). Para determinar se uma página foi descartada em uma guia
escondida, inspecione o valor dessa propriedade no momento do carregamento da página. Observe que
páginas descartadas precisam ser recarregadas para serem usadas novamente.
if (document.wasDiscarded) {
// Page was previously discarded by the browser while in a hidden tab.
}
Para receber conselhos sobre o que é importante fazer nos eventos freeze
e resume
, bem como como processar e se preparar para a eliminação de páginas, consulte
recomendações para desenvolvedores de cada estado.
As próximas seções oferecem uma visão geral de como esses novos recursos se encaixam nos estados e eventos da plataforma da Web.
Como observar os estados do ciclo de vida da página no código
Nos estados ativo, passivo e oculto, é possível executar um código JavaScript que determina o estado atual do ciclo de vida da página nas APIs da plataforma da Web.
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
Os estados congelado e encerrado, por
outro lado, só podem ser detectados no respectivo listener de eventos
(freeze
e pagehide
) à medida que o estado está
mudando.
Como observar mudanças de estado
Com base na função getState()
definida anteriormente, você pode observar todas as mudanças de estado
do ciclo de vida da página com o código abaixo.
// 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);
Esse código faz três coisas:
- Define o estado inicial usando a função
getState()
. - Define uma função que aceita um próximo estado e, se houver uma mudança, registra as mudanças de estado no console.
- Adiciona listeners de eventos de
captura
para todos os eventos de ciclo de vida necessários, que por sua vez chamam
logStateChange()
, transmitindo o próximo estado.
Uma observação sobre o código é que todos os listeners de eventos são adicionados
a window
e todos eles transmitem
{capture: true}
.
Existem alguns motivos para isso acontecer, entre eles:
- Nem todos os eventos do ciclo de vida da página têm o mesmo destino.
pagehide
epageshow
são acionados emwindow
.visibilitychange
,freeze
eresume
são acionados emdocument
, efocus
eblur
são acionados nos respectivos elementos DOM. - A maioria desses eventos não flutua, o que significa que é impossível adicionar detectores de eventos não de captura a um elemento ancestral comum e observar todos eles.
- A fase de captura é executada antes das fases de destino ou de bolha. Portanto, adicionar listeners garante que eles sejam executados antes que outros códigos possam cancelá-los.
Recomendações para desenvolvedores em cada estado
Como desenvolvedores, é importante entender os estados do ciclo de vida da página e saber como observá-los no código, porque o tipo de trabalho que você precisa (e não precisa) fazer depende muito do estado da sua página.
Por exemplo, não faz sentido mostrar uma notificação temporária ao usuário se a página estiver no estado oculto. Embora esse exemplo seja bastante óbvio, há outras recomendações que não são tão óbvias e valem a pena enumerar.
Estado | Recomendações para desenvolvedores |
---|---|
Active |
O estado ativo é o momento mais crítico para o usuário e, portanto, o momento mais importante para que sua página seja responsiva à entrada do usuário. Qualquer trabalho que não seja da interface que possa bloquear a linha de execução principal precisa ter a prioridade reduzida para períodos de inatividade ou transferido para um worker da Web. |
Passive |
No estado passivo, o usuário não está interagindo com a página, mas ainda pode visualizá-la. Isso significa que as atualizações e animações da interface ainda precisam ser suaves, mas o momento em que essas atualizações ocorrem é menos crítico. Quando a página muda de ativa para passiva, é um bom momento para persistir o estado do aplicativo não salvo. |
Quando a página muda de passiva para oculta, é possível que o usuário não interaja com ela novamente até que ela seja recarregada. A transição para hidden também é geralmente a última mudança de estado
que pode ser observada com confiabilidade pelos desenvolvedores. Isso é especialmente verdadeiro em
dispositivos móveis, já que os usuários podem fechar guias ou o próprio app do navegador, e os
eventos Isso significa que você deve tratar o estado hidden como o provável fim da sessão do usuário. Em outras palavras, persista qualquer estado de aplicativo não salvo e envie todos os dados de análise não enviados. Também é preciso parar de fazer atualizações da interface (já que elas não serão vistas pelo usuário) e interromper todas as tarefas que um usuário não quer que sejam executadas em segundo plano. |
|
Frozen |
No estado congelado, tarefas que podem ser congeladas nas filas de tarefas são suspensas até que a página seja descongelada, o que pode nunca acontecer (por exemplo, se a página for descartada). Isso significa que, quando a página muda de oculta para congelada, é essencial interromper os timers ou remover as conexões que, se congeladas, podem afetar outras guias abertas na mesma origem ou a capacidade do navegador de colocar a página no cache de ida/volta. Em particular, é importante que você:
Você também precisa manter qualquer estado de visualização dinâmico (por exemplo, posição de rolagem
em uma visualização de lista infinita) para
Se a página passar de congelada para oculta, você poderá reabrir as conexões fechadas ou reiniciar as pesquisas interrompidas quando a página foi congelada. |
Terminated |
Geralmente, não é necessário fazer nada quando uma página transita para o estado encerrado. Como as páginas que estão sendo descarregadas como resultado da ação do usuário sempre passam pelo estado oculto antes de entrar no estado encerrado, o estado oculto é onde a lógica de encerramento de sessão (por exemplo, persistência do estado do aplicativo e geração de relatórios para a análise) precisa ser realizada. Além disso, conforme mencionado nas recomendações para
o estado hidden, é muito importante que os desenvolvedores entendam
que a transição para o estado terminated não pode ser detectada
de forma confiável em muitos casos (especialmente em dispositivos móveis). Portanto, os desenvolvedores que dependem
de eventos de encerramento (por exemplo, |
Discarded |
O estado descartado não é observável pelos desenvolvedores no momento em que uma página está sendo descartada. Isso ocorre porque as páginas geralmente são descartadas em restrições de recursos, e descongelar uma página apenas para permitir que o script seja executado em resposta a um evento de descarte simplesmente não é possível na maioria dos casos. Como resultado, prepare-se para a possibilidade de uma rejeição na
mudança de hidden para frozen. Em seguida, você pode
reagir à restauração de uma página descartada no momento do carregamento da página
verificando |
Mais uma vez, como a confiabilidade e a ordem dos eventos do ciclo de vida não são implementadas de forma consistente em todos os navegadores, a maneira mais fácil de seguir o conselho na tabela é usar PageLifecycle.js.
APIs de ciclo de vida legadas a evitar
Evite os eventos a seguir sempre que possível.
O evento de descarregamento
Muitos desenvolvedores tratam o evento unload
como um callback garantido e o usam como
um indicador de fim de sessão para salvar o estado e enviar dados de análise, mas isso
é extremamente confiável, especialmente em dispositivos móveis. O evento unload
não
é acionado em muitas situações de remoção típicas, incluindo o fechamento de uma guia do
comutador de guias em dispositivos móveis ou o fechamento do app de navegação do comutador de apps.
Por esse motivo, é sempre melhor usar o evento
visibilitychange
para determinar quando uma sessão
termina e considerar o estado oculto como o
último momento confiável para salvar dados do app e do usuário.
Além disso, a mera presença de um manipulador de eventos unload
registrado (por
onunload
ou addEventListener()
) pode impedir que os navegadores
coloquem páginas no cache de avanço e retorno para carregamentos
mais rápidos.
Em todos os navegadores modernos, é recomendável sempre usar o evento
pagehide
para detectar possíveis descarregamentos de página (também conhecido como
estado encerrado) em vez do evento unload
. Se você
precisar oferecer suporte às versões 10 e anteriores do Internet Explorer, detecte
o evento pagehide
e use unload
apenas se o navegador não oferecer suporte a
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.
});
O evento beforeunload
O evento beforeunload
tem um problema semelhante ao evento unload
, em que, historicamente, a presença de um evento beforeunload
pode impedir que as páginas
sejam qualificadas para o cache de avanço e retorno. Os navegadores modernos
não têm essa restrição. Embora alguns navegadores, como precaução, não gerem
o evento beforeunload
ao tentar colocar uma página no cache de ida/volta, o que significa que o evento não é confiável como um indicador de fim de sessão.
Além disso, alguns navegadores (incluindo o Chrome)
exigem uma interação do usuário na página antes de permitir que o evento beforeunload
seja acionado, afetando ainda mais a confiabilidade.
Uma diferença entre beforeunload
e unload
é que há
usos legítimos de beforeunload
. Por exemplo, quando você quer avisar o usuário
que ele tem alterações não salvas que serão perdidas se ele continuar descarregando a página.
Como há motivos válidos para usar beforeunload
, é recomendável
só adicionar listeners de beforeunload
quando um usuário tiver mudanças não salvas e, em seguida,
remover esses listeners.
Em outras palavras, não faça isso (já que ele adiciona um listener beforeunload
incondicionalmente):
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);
}
});
Em vez disso, faça o seguinte (já que ele só adiciona o listener beforeunload
quando necessário
e o remove quando não é necessário):
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);
});
Perguntas frequentes
Por que não há um estado "Carregando"?
A API Page Lifecycle define estados discretos e mutuamente exclusivos. Como uma página pode ser carregada no estado ativo, passivo ou oculto e como ela pode mudar de estado ou até ser encerrada antes de terminar o carregamento, um estado de carregamento separado não faz sentido nesse paradigma.
Minha página faz um trabalho importante quando está oculta. Como posso evitar que ela seja congelada ou descartada?
Há muitos motivos legítimos para que as páginas da Web não sejam congeladas durante a execução no estado oculto. O exemplo mais óbvio é um app que toca música.
Também há situações em que seria arriscado o Chrome descartar uma página,
como se ela contiver um formulário com entrada do usuário não enviada ou se tiver um
gerenciador beforeunload
que avise quando a página estiver sendo removida.
Por enquanto, o Chrome vai ser conservador ao descartar páginas e só fará isso quando tiver certeza de que não afetará os usuários. Por exemplo, as páginas que foram observadas fazendo qualquer uma das seguintes ações enquanto estão no estado oculto não serão descartadas, a menos que estejam sob restrições de recursos extremas:
- Reprodução de áudio
- Como usar o WebRTC
- Como atualizar o título ou o ícone da tabela
- Como mostrar alertas
- Como enviar notificações push
Para conferir os recursos de lista atuais usados para determinar se uma guia pode ser congelada ou descartada com segurança, consulte: Heuristics for Freezing & Discarding no Chrome.
O que é o cache de avanço e retorno?
O cache de avanço e retorno é um termo usado para descrever uma otimização de navegação implementada por alguns navegadores que acelera o uso dos botões "Voltar" e "Avançar".
Quando um usuário sai de uma página, esses navegadores congelam uma versão dela
para que ela possa ser retomada rapidamente caso o usuário volte usando
os botões "Voltar" ou "Avançar". Lembre-se de que adicionar um manipulador de eventos unload
impede essa otimização.
Para todos os fins e propósitos, esse congelamento é funcionalmente o mesmo que os navegadores de congelamento executam para economizar CPU/bateria. Por esse motivo, ele é considerado parte do estado de ciclo de vida congelado.
Se não for possível executar APIs assíncronas nos estados congelado ou encerrado, como posso salvar dados no IndexedDB?
Em estados congelados e encerrados, tarefas que podem ser congeladas nas filas de tarefas de uma página são suspensas, o que significa que APIs assíncronas e baseadas em callback, como a IndexedDB, não podem ser usadas de forma confiável.
No futuro, vamos adicionar um método commit()
aos objetos IDBTransaction
, o que vai
dar aos desenvolvedores uma maneira de realizar transações somente de gravação
que não exigem callbacks. Em outras palavras, se o desenvolvedor estiver apenas gravando
dados no IndexedDB e não executando uma transação complexa que consiste em leituras
e gravações, o método commit()
poderá ser concluído antes que as filas de tarefas sejam
interrompidas (assumindo que o banco de dados IndexedDB já esteja aberto).
No entanto, para o código que precisa funcionar hoje, os desenvolvedores têm duas opções:
- Usar o armazenamento de sessão:o armazenamento de sessão é síncrono e persiste em descartes de página.
- Use o IndexedDB no service worker:um service worker pode armazenar dados no
IndexedDB depois que a página é encerrada ou descartada. No listener de evento
freeze
oupagehide
, é possível enviar dados para o service worker usandopostMessage()
, e o service worker pode salvar os dados.
Como testar o app nos estados congelado e descartado
Para testar como o app se comporta nos estados congelado e descartado, acesse
chrome://discards
para congelar ou descartar qualquer uma das
guias abertas.
Isso permite que você garanta que a página processe corretamente os eventos freeze
e resume
,
bem como a flag document.wasDiscarded
, quando as páginas forem recarregadas após
um descarte.
Resumo
Os desenvolvedores que querem respeitar os recursos do sistema dos dispositivos dos usuários precisam criar apps pensando nos estados do ciclo de vida da página. É essencial que as páginas da Web não consumam recursos excessivos do sistema em situações que o usuário não espera.
Quanto mais desenvolvedores começarem a implementar as novas APIs do ciclo de vida da página, mais seguro será para os navegadores congelar e descartar páginas que não estão sendo usadas. Isso significa que os navegadores vão consumir menos memória, CPU, bateria e recursos de rede, o que é uma vitória para os usuários.