Navegadores compatibles
En la actualidad, los navegadores modernos a veces suspenden páginas o las descartan por completo cuando los recursos del sistema están limitados. En el futuro, los navegadores querrán hacer esto de forma proactiva para que consuman menos energía y memoria. La API de Page Lifecycle proporciona hooks de ciclo de vida para que tus páginas puedan controlar de forma segura estas intervenciones del navegador sin afectar la experiencia del usuario. Consulta la API para ver si debes implementar estas funciones en tu aplicación.
Segundo plano
El ciclo de vida de las aplicaciones es una forma clave en que los sistemas operativos modernos administran los recursos. En Android, iOS y versiones recientes de Windows, el SO puede iniciar y detener apps en cualquier momento. Esto permite a estas plataformas optimizar y reasignar recursos donde mejor beneficien al usuario.
En la Web, históricamente, no ha existido ese ciclo de vida, y las apps se pueden mantener activas de forma indefinida. Con una gran cantidad de páginas web en ejecución, los recursos del sistema críticos, como la memoria, la CPU, la batería y la red, pueden tener una suscripción excesiva, lo que genera una mala experiencia del usuario final.
Si bien la plataforma web tiene desde hace mucho tiempo eventos relacionados con los estados del ciclo de vida, como load
, unload
y visibilitychange
, estos eventos solo permiten que los desarrolladores respondan a los cambios de estado del ciclo de vida que inicia el usuario. Para que la Web funcione de manera confiable en dispositivos de baja potencia (y sea más consciente de los recursos en general en todas las plataformas), los navegadores necesitan una forma de recuperar y reasignar de forma proactiva los recursos del sistema.
De hecho, los navegadores actuales ya toman medidas activas para conservar recursos en las páginas de las pestañas en segundo plano, y muchos navegadores (en especial, Chrome) les gustaría hacer mucho más para reducir su huella de recursos general.
El problema es que los desarrolladores no tienen forma de prepararse para estos tipos de intervenciones iniciadas por el sistema ni de saber que se están realizando. Esto significa que los navegadores deben ser conservadores o correr el riesgo de dañar las páginas web.
La API de Page Lifecycle intenta resolver este problema de la siguiente manera:
- Presentamos y estandarizamos el concepto de estados del ciclo de vida en la Web.
- Definir estados nuevos iniciados por el sistema que permitan a los navegadores limitar los recursos que pueden consumir las pestañas ocultas o inactivas
- Crear APIs y eventos nuevos que permitan a los desarrolladores web responder a transiciones hacia y desde estos nuevos estados iniciados por el sistema
Esta solución proporciona la previsibilidad que necesitan los desarrolladores web para compilar aplicaciones resistentes a las intervenciones del sistema y permite que los navegadores optimicen de forma más agresiva los recursos del sistema, lo que, en última instancia, beneficia a todos los usuarios web.
En el resto de esta publicación, se presentarán las nuevas funciones del ciclo de vida de la página y se explorará cómo se relacionan con todos los estados y eventos existentes de la plataforma web. También se proporcionarán recomendaciones y prácticas recomendadas para los tipos de trabajo que los desarrolladores deberían (y no deberían) realizar en cada estado.
Descripción general de los estados y eventos del ciclo de vida de la página
Todos los estados del ciclo de vida de la página son discretos y se excluyen mutuamente, lo que significa que una página solo puede estar en un estado a la vez. Además, la mayoría de los cambios en el estado del ciclo de vida de una página se pueden observar a través de eventos de DOM (consulta las recomendaciones para desarrolladores para cada estado si quieres conocer las excepciones).
Quizás la forma más sencilla de explicar los estados del ciclo de vida de la página, así como los eventos que indican las transiciones entre ellos, es con un diagrama:
Estados
En la siguiente tabla, se explica cada estado en detalle. También enumera los posibles estados que pueden ocurrir antes y después, así como los eventos que los desarrolladores pueden usar para observar los cambios.
Estado | Descripción |
---|---|
Activo |
Una página está en el estado activo si es visible y tiene el foco de entrada.
Posibles estados anteriores:
Posibles estados siguientes: |
Pasivo |
Una página está en el estado pasivo si es visible y no tiene el foco de entrada.
Posibles estados anteriores:
Posibles estados siguientes: |
Oculto |
Una página se encuentra en el estado oculto si no es visible (y no se congeló, descartó ni finalizó).
Posibles estados anteriores:
Posibles estados siguientes: |
Congelado |
En el estado inmovilizado, el navegador suspende la ejecución de las tareas inmovilizables en las colas de tareas de la página hasta que se desinmoviliza la página. Esto significa que no se ejecutan elementos como
los temporizadores de JavaScript y las devoluciones de llamada de recuperación. Las tareas que ya están en ejecución pueden finalizar (lo más importante es la devolución de llamada de Los navegadores inmovilizan las páginas como una forma de preservar el uso de la CPU, la batería y los datos. También lo hacen para permitir navegaciones hacia atrás o hacia adelante más rápidas, lo que evita la necesidad de volver a cargar una página completa.
Posibles estados anteriores:
Posibles estados siguientes: |
Finalizada |
Una página se encuentra en el estado terminada una vez que el navegador comienza a descargarla y borrarla de la memoria. No se pueden iniciar tareas nuevas en este estado, y es posible que se finalicen las tareas en curso si se ejecutan demasiado tiempo.
Posibles estados anteriores:
Próximos estados posibles: |
Descartado |
Una página se encuentra en el estado descartada cuando el navegador la descarga para conservar recursos. No se pueden ejecutar tareas, devoluciones de llamadas de eventos ni JavaScript de ningún tipo en este estado, ya que los descartes suelen ocurrir bajo restricciones de recursos, en las que es imposible iniciar procesos nuevos. En el estado descartado, el usuario suele ver la pestaña (incluido el título y el favicon de la pestaña) aunque la página ya no esté.
Posibles estados anteriores:
Posibles estados siguientes: |
Eventos
Los navegadores envían muchos eventos, pero solo una pequeña parte de ellos indica un posible cambio en el estado del ciclo de vida de la página. En la siguiente tabla, se describen todos los eventos que pertenecen al ciclo de vida y se enumeran los estados a los que pueden realizar la transición.
Nombre | Detalles |
---|---|
focus
|
Un elemento DOM recibió el foco.
Nota: Un evento
Posibles estados anteriores:
Posibles estados actuales: |
blur
|
Un elemento del DOM perdió el foco.
Nota: Un evento
Estados anteriores posibles:
Estados actuales posibles: |
visibilitychange
|
Cambió el valor de |
freeze
*
|
La página se acaba de inmovilizar. No se iniciará ninguna tarea inmovilizable en las listas de tareas en cola de la página.
Posibles estados anteriores:
Estados actuales posibles: |
resume
*
|
El navegador reanudó una página congelada.
Estados anteriores posibles:
Posibles estados actuales: |
pageshow
|
Se está atravesando una entrada del historial de sesión. Puede ser una carga de página nueva o una página tomada de la memoria caché atrás/adelante. Si la página se tomó de la memoria caché atrás/adelante, la propiedad
Posibles estados anteriores: |
pagehide
|
Se realiza un recorrido de una entrada del historial de sesión. Si el usuario navega a otra página y el navegador puede agregar la página actual a la memoria caché atrás/adelante para volver a usarla más tarde, la propiedad
Posibles estados anteriores:
Posibles estados actuales: |
beforeunload
|
La ventana, el documento y sus recursos están a punto de descargarse. El documento aún es visible y el evento se puede cancelar en este punto.
Importante: El evento
Estados anteriores posibles:
Estados actuales posibles: |
unload
|
Se está descargando la página.
Advertencia: Nunca se recomienda usar el evento
Estados anteriores posibles:
Posibles estados actuales: |
* Indica un evento nuevo definido por la API de Page Lifecycle.
Funciones nuevas agregadas en Chrome 68
En el gráfico anterior, se muestran dos estados iniciados por el sistema en lugar de por el usuario: congelado y descartado. Como se mencionó anteriormente, los navegadores hoy en día, en ocasiones, inmovilizan y descartan pestañas ocultas (a su discreción), pero los desarrolladores no tienen forma de saber cuándo sucede esto.
En Chrome 68, los desarrolladores ahora pueden detectar los eventos freeze
y resume
en document
para observar cuándo se bloquea y se desbloqueó una pestaña oculta.
document.addEventListener('freeze', (event) => {
// The page is now frozen.
});
document.addEventListener('resume', (event) => {
// The page has been unfrozen.
});
A partir de Chrome 68, el objeto document
ahora incluye una propiedad wasDiscarded
en Chrome para computadoras (se está haciendo un seguimiento de la compatibilidad con Android en este problema). Para determinar si se descartó una página mientras estaba en una pestaña oculta, puedes inspeccionar el valor de esta propiedad en el tiempo de carga de la página (nota: las páginas descartadas se deben volver a cargar para volver a usarlas).
if (document.wasDiscarded) {
// Page was previously discarded by the browser while in a hidden tab.
}
Si deseas obtener sugerencias sobre qué es importante hacer en los eventos freeze
y resume
, así como cómo controlar y prepararte para que se descarten páginas, consulta las recomendaciones para desarrolladores para cada estado.
En las siguientes secciones, se ofrece una descripción general de cómo estas funciones nuevas se ajustan a los estados y eventos existentes de la plataforma web.
Cómo observar los estados del ciclo de vida de la página en el código
En los estados activo, pasivo y oculto, es posible ejecutar código JavaScript que determine el estado actual del ciclo de vida de la página desde las APIs de la plataforma web existentes.
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
Por otro lado, los estados congelado y finalizado solo se pueden detectar en sus respectivos objetos de escucha de eventos (freeze
y pagehide
) a medida que cambia el estado.
Cómo observar cambios de estado
Con base en la función getState()
definida anteriormente, puedes observar todos los cambios de estado del ciclo de vida de la página con el siguiente código.
// 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);
Este código realiza tres acciones:
- Establece el estado inicial con la función
getState()
. - Define una función que acepta un estado siguiente y, si hay un cambio, registra los cambios de estado en la consola.
- Agrega objetos de escucha de eventos de captura para todos los eventos de ciclo de vida necesarios, que a su vez llaman a
logStateChange()
y pasan el siguiente estado.
Una cosa que debes tener en cuenta sobre el código es que todos los objetos de escucha de eventos se agregan a window
y todos pasan {capture: true}
.
Esto se debe a varios motivos:
- No todos los eventos del ciclo de vida de la página tienen el mismo objetivo.
pagehide
ypageshow
se activan enwindow
;visibilitychange
,freeze
yresume
se activan endocument
, yfocus
yblur
se activan en sus respectivos elementos DOM. - La mayoría de estos eventos no se propagan, lo que significa que es imposible agregar objetos de escucha de eventos que no capturen a un elemento ancestro común y observarlos todos.
- La fase de captura se ejecuta antes que las fases de destino o de burbujas, por lo que agregar objetos de escucha allí ayuda a garantizar que se ejecuten antes de que otro código pueda cancelarlos.
Recomendaciones para desarrolladores para cada estado
Como desarrolladores, es importante comprender los estados del ciclo de vida de las páginas y saber cómo observarlos en el código, ya que el tipo de trabajo que debes (y no debes) hacer depende en gran medida del estado de tu página.
Por ejemplo, no tiene sentido mostrarle una notificación transitoria al usuario si la página está en el estado oculto. Si bien este ejemplo es bastante obvio, hay otras recomendaciones que no son tan obvias y que vale la pena enumerar.
Estado | Recomendaciones para desarrolladores |
---|---|
Active |
El estado activo es el momento más importante para el usuario y, por lo tanto, el momento más importante para que tu página sea sensible a la entrada del usuario. Cualquier trabajo que no sea de la IU que pueda bloquear el subproceso principal debe tener una prioridad menor en los períodos inactivos o transferirse a un trabajador web. |
Passive |
En el estado pasivo, el usuario no interactúa con la página, pero aún puede verla. Esto significa que las actualizaciones y animaciones de la IU aún deben ser fluidas, pero el momento en que ocurren estas actualizaciones es menos importante. Cuando la página cambia de activa a pasiva, es un buen momento para conservar el estado de la aplicación no guardado. |
Cuando la página cambia de pasiva a oculta, es posible que el usuario no vuelva a interactuar con ella hasta que se vuelva a cargar. La transición a hidden también suele ser el último cambio de estado que los desarrolladores pueden observar de forma confiable (esto es especialmente cierto en dispositivos móviles, ya que los usuarios pueden cerrar pestañas o la app del navegador en sí, y los eventos Esto significa que debes tratar el estado oculto como el final probable de la sesión del usuario. En otras palabras, conserva cualquier estado de la aplicación que no se haya guardado y envía los datos de estadísticas que no se hayan enviado. También debes dejar de realizar actualizaciones de la IU (ya que el usuario no las verá) y detener las tareas que un usuario no querría que se ejecuten en segundo plano. |
|
Frozen |
En el estado inmovilizado, las tareas inmovilizables en las colas de tareas se suspenden hasta que se desinmoviliza la página, lo que puede que nunca suceda (p. ej., si se descarta la página). Esto significa que, cuando la página cambie de ocultada a congelada, es fundamental que detengas cualquier temporizador o desconectes cualquier conexión que, si se congela, podría afectar a otras pestañas abiertas en el mismo origen o a la capacidad del navegador para colocar la página en la caché de atrás/adelante. En particular, es importante que hagas lo siguiente:
También debes conservar cualquier estado de vista dinámico (p. ej., la posición de desplazamiento en una vista de lista infinita) en Si la página pasa de congelada a oculta, puedes volver a abrir las conexiones cerradas o reiniciar las sondeos que detuviste cuando la página se congeló inicialmente. |
Terminated |
Por lo general, no es necesario que realices ninguna acción cuando una página realiza la transición al estado terminated. Dado que las páginas que se descargan como resultado de la acción del usuario siempre pasan por el estado oculto antes de ingresar al estado finalizado, el estado oculto es donde se debe realizar la lógica de finalización de la sesión (p. ej., conservar el estado de la aplicación y generar informes para Analytics). Además (como se menciona en las recomendaciones para el estado oculto), es muy importante que los desarrolladores comprendan que la transición al estado terminado no se puede detectar de forma confiable en muchos casos (especialmente en dispositivos móviles), por lo que es probable que los desarrolladores que dependen de eventos de terminación (p. ej., |
Discarded |
Los desarrolladores no pueden observar el estado descartado en el momento en que se descarta una página. Esto se debe a que, por lo general, las páginas se descartan debido a restricciones de recursos, y no es posible descongelar una página solo para permitir que la secuencia de comandos se ejecute en respuesta a un evento de descarte en la mayoría de los casos. Como resultado, debes prepararte para la posibilidad de que se descarte el cambio de oculto a congelado y, luego, puedes reaccionar al restablecimiento de una página descartada en el tiempo de carga de la página verificando |
Una vez más, dado que la confiabilidad y el orden de los eventos del ciclo de vida no se implementan de manera coherente en todos los navegadores, la forma más fácil de seguir los consejos de la tabla es usar PageLifecycle.js.
APIs de ciclo de vida heredados que se deben evitar
Siempre que sea posible, se deben evitar los siguientes eventos.
El evento de descarga
Muchos desarrolladores tratan el evento unload
como una devolución de llamada garantizada y lo usan como un indicador de fin de sesión para guardar el estado y enviar datos de estadísticas, pero hacer esto no es confiable, especialmente en dispositivos móviles. El evento unload
no se activa en muchas situaciones típicas de descarga, como cerrar una pestaña desde el selector de pestañas en dispositivos móviles o cerrar la app de navegador desde el selector de apps.
Por este motivo, siempre es mejor confiar en el evento visibilitychange
para determinar cuándo finaliza una sesión y considerar el estado oculto como el último momento confiable para guardar datos de la app y del usuario.
Además, la mera presencia de un controlador de eventos unload
registrado (a través de onunload
o addEventListener()
) puede evitar que los navegadores puedan colocar páginas en la memoria caché atrás/adelante para cargas de atrás y adelante más rápidas.
En todos los navegadores modernos, se recomienda usar siempre el evento pagehide
para detectar posibles descargas de páginas (también conocido como estado terminated) en lugar del evento unload
. Si necesitas admitir Internet Explorer 10 y versiones anteriores, debes detectar el evento pagehide
y usar solo unload
si el navegador no admite 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.
});
El evento beforeunload
El evento beforeunload
tiene un problema similar al evento unload
, en el sentido de que, históricamente, la presencia de un evento beforeunload
podría impedir que las páginas sean aptas para la memoria caché atrás/adelante. Los navegadores modernos no tienen esta restricción. Sin embargo, algunos navegadores, como precaución, no activarán el evento beforeunload
cuando intenten colocar una página en la caché de atrás/adelante, lo que significa que el evento no es confiable como indicador de fin de sesión.
Además, algunos navegadores (incluido Chrome) requieren una interacción del usuario en la página antes de permitir que se active el evento beforeunload
, lo que afecta aún más su confiabilidad.
Una diferencia entre beforeunload
y unload
es que beforeunload
tiene usos legítimos. Por ejemplo, cuando quieras advertirle al usuario que tiene cambios sin guardar que perderá si continúa descargando la página.
Dado que existen razones válidas para usar beforeunload
, te recomendamos que solo agregues objetos de escucha beforeunload
cuando un usuario tenga cambios sin guardar y que, luego, los quites inmediatamente después de guardarlos.
En otras palabras, no hagas lo siguiente (ya que agrega un objeto de escucha beforeunload
de forma incondicional):
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);
}
});
En su lugar, haz lo siguiente (ya que solo agrega el objeto de escucha beforeunload
cuando es necesario y lo quita cuando no lo es):
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);
});
Preguntas frecuentes
¿Por qué no aparece el estado "cargando"?
La API de Page Lifecycle define los estados como discretos y mutuamente excluyentes. Dado que una página se puede cargar en el estado activo, pasivo o oculto, y puede cambiar de estado (o incluso finalizarse) antes de que termine de cargarse, un estado de carga independiente no tiene sentido dentro de este paradigma.
Mi página realiza tareas importantes cuando está oculta. ¿Cómo puedo evitar que se congele o se descarte?
Existen muchos motivos legítimos por los que las páginas web no deberían inmovilizarse mientras se ejecutan en estado oculto. El ejemplo más obvio es una app que reproduce música.
También hay situaciones en las que sería riesgoso que Chrome descartara una página, por ejemplo, si contiene un formulario con una entrada del usuario que no se envió o si tiene un controlador beforeunload
que advierte cuando se descarga la página.
Por el momento, Chrome será conservador cuando descarte páginas y solo lo hará cuando tenga la certeza de que no afectará a los usuarios. Por ejemplo, las páginas que se observaron haciendo alguna de las siguientes acciones mientras estaban en el estado oculto no se descartarán, a menos que se encuentren en restricciones de recursos extremas:
- Cómo reproducir audio
- Cómo usar WebRTC
- Actualiza el título de la tabla o el ícono de página
- Cómo mostrar alertas
- Envío de notificaciones push
Para conocer las funciones de la lista actuales que se usan para determinar si una pestaña se puede inhabilitar o descartar de forma segura, consulta Heuristics for Freezing & Discarding en Chrome.
¿Qué es la memoria caché atrás/adelante?
La memoria caché de atrás/adelante es un término que se usa para describir una optimización de navegación que implementan algunos navegadores y que hace que el uso de los botones atrás y adelante sea más rápido.
Cuando un usuario sale de una página, estos navegadores inmovilizan una versión de esa página para que se pueda reanudar rápidamente en caso de que el usuario vuelva a navegar con los botones Atrás o Adelante. Recuerda que agregar un controlador de eventos unload
impide que esta optimización sea posible.
A todos los efectos, esta inmovilización es funcionalmente la misma que realizan los navegadores para conservar la CPU o la batería. Por esa razón, se considera parte del estado del ciclo de vida inmovilizado.
Si no puedo ejecutar APIs asíncronas en estado congelado o finalizado, ¿cómo puedo guardar datos en IndexedDB?
En los estados suspendido y cerrado, las tareas que se pueden suspender en las colas de tareas de una página se suspenden, lo que significa que no se pueden usar de forma confiable las APIs asíncronas y basadas en devoluciones de llamada, como IndexedDB.
En el futuro, agregaremos un método commit()
a los objetos IDBTransaction
, lo que les dará a los desarrolladores una forma de realizar transacciones de solo escritura que no requieran devoluciones de llamada. En otras palabras, si el desarrollador solo escribe datos en IndexedDB y no realiza una transacción compleja que consta de operaciones de lectura y escritura, el método commit()
podrá finalizar antes de que se suspendan las filas de tareas (suponiendo que la base de datos de IndexedDB ya está abierta).
Sin embargo, para el código que debe funcionar hoy, los desarrolladores tienen dos opciones:
- Usa el almacenamiento de sesión: El almacenamiento de sesión es síncrono y se conserva en todas las descartes de páginas.
- Usa IndexedDB del service worker: Un service worker puede almacenar datos en
IndexedDB después de que la página se haya cerrado o descartado. En el objeto de escucha de eventos
freeze
opagehide
, puedes enviar datos a tu service worker a través depostMessage()
, y el service worker puede controlar el guardado de los datos.
Cómo probar tu app en los estados inmovilizado y descartado
Para probar cómo se comporta tu app en los estados inactivos y descartados, puedes visitar chrome://discards
para inhabilitar o descartar cualquiera de tus pestañas abiertas.
Esto te permite asegurarte de que tu página controle correctamente los eventos freeze
y resume
, así como la marca document.wasDiscarded
cuando se vuelvan a cargar las páginas después de un descarte.
Resumen
Los desarrolladores que quieran respetar los recursos del sistema de los dispositivos de sus usuarios deben compilar sus apps teniendo en cuenta los estados del ciclo de vida de la página. Es fundamental que las páginas web no consuman demasiados recursos del sistema en situaciones que el usuario no esperaría.
Cuantos más desarrolladores comiencen a implementar las nuevas APIs de ciclo de vida de la página, más seguro será para los navegadores inmovilizar y descartar las páginas que no se están usando. Esto significa que los navegadores consumirán menos recursos de memoria, CPU, batería y red, lo que es una ventaja para los usuarios.