Este post es la respuesta que le dí, hace ya unos meses, a la pregunta de uno de mis alumnos de mi curso de Fundamentos de desarrollo web con JavaScript y jQuery en campusMVP. Quería hacer ciertas llamadas AJAX al servidor desde eventos de descarga de la página, pero tenía problemas, especialmente en navegadores móviles. Transcribo mi respuesta, casi sin tocar nada y añadiendo algunas negritas para facilitar la lectura, porque creo que puede ser interesante para muchos de los lectores de este blog, puesto que contiene mucha información sobre cómo funcionan los eventos de descarga de las páginas y todas las complejidades que presentan.

Imagen ornamentalAunque todos los desarrolladores Web tienen más o menos claro el funcionamiento de los eventos unload, beforeunload y otros similares en un navegador de escritorio, el tema de la descarga de páginas en los navegadores móviles es bastante complejo por la forma particular que tienen de funcionar respecto a un navegador de escritorio. Es largo de contar y con muchas ramificaciones, pero voy a intentar resumirlo.

Para empezar, un navegador móvil es una app móvil (obviamente) y por lo tanto tiene un ciclo de vida muy diferente al de una aplicación de escritorio. En el escritorio, si minimizas la ventana o cambias a otra aplicación digamos que no hay diferencia con tenerla en primer plano salvo por que no tiene el foco de entrada de datos. En el móvil esto es muy distinto ya que las aplicaciones pasan automáticamente a un estado de suspensión instantáneo cuando las minimizas (yendo al escritorio) o cambiando a otra aplicación. En ese estado realmente la información de la página no cambia (y por lo tanto tampoco hay una descarga de la misma) porque se mantiene todo en memoria, pero pueden "morir" en cualquier momento si el sistema operativo decide que necesita memoria libre, por ejemplo, en cuyo caso sí que se puede cerrar la página y quizá tengas la opción de capturar un evento unload o similar (luego explico más ese "quizá").

Por cierto, en ese estado de suspensión los temporizadores tampoco funcionan (o sea, si tienes un intervalo establecido con setInterval() no se va a ejecutar cada "x" segundos como si estuviese en primer plano, porque está suspendida), pero luego cuando el usuario traiga de nuevo la aplicación a primer plano se reanudan en el punto en el que estaban (con la hora actualizada).

En fin, como podemos ver, es un tema complejo. Pero es que además hay otra cuestión importante y es que no todos los navegadores funcionan igual. Para empezar hay que distinguir entre iOS y Android.

En iOS todos los navegadores son el mismo: Safari. El motivo es que Apple no permite meter motores de renderizado propios y todos los navegadores son en realidad una capa de funcionalidad por encima de la versión de Safari que tenga el sistema operativo. Lo cual, de hecho, es bueno porque unifica mucho y facilita el trabajo, aunque limita la innovación.

En Android la cosa es diferente. La mayor parte de los navegadores (por ejemplo Edge o Brave) usan Chrome por debajo, lo cual es fenomenal porque ayuda a unificar. Otros usan su propio motor, como por ejemplo Firefox, Opera Mobile y algunos de los navegadores propios de los fabricantes de móviles.

Chrome y los basados en él funcionan muy bien y muy pegados a los estándares, y Firefox en sus últimas versiones también. Por ello, por ejemplo, Chrome y Firefox sí que lanzan los eventos unload y relacionados cuando cambias de una página a otra al navegar (no así al minimizar, por lo que ya he mencionado), comportándose como un navegador de escritorio en ese sentido. Sin embargo otros navegadores como el de Xiaomi, el de Samsung o el famoso navegador chino UC Browser que usa alguna gente también por aquí (🙋🏻‍♂️,para pruebas de desarrollo), trabajan de una forma muy particular que dificulta las cosas. Cuando en uno de estos navegadores navegas entre páginas, en lugar de cerrar la página anterior (y por lo tanto provocar un unload) lo que hacen es algo parecido a lo que hace el sistema operativo: dejan la página en segundo plano "congelada" de modo que puedes volver a ella fácilmente dándole para atrás, pero no la descargan. Como no la descargan, los eventos de descarga como los mencionados no saltan nunca. Safari en iOS hace algo parecido.

Para terminar con los problemas y luego ir a la solución, debemos saber también que, aunque nos funcionasen los eventos de descarga, tanto en el evento beforeunload como en el evento unload hay muchas cosas que no puedes hacer.

En beforeunload no puedes interactuar con el usuario más que devolviendo una cadena de texto que se mostrará en el mensaje de confirmación de descarga, que puede además cancelar y que el usuario no llegue a salir de la página. En el evento unload todo código que metamos debe ser muy breve o puede que no te dé tiempo a que se ejecute. Pero es que además en algunos navegadores no se admiten llamadas AJAX asíncronas (por ejemplo en Safari) y si las haces simplemente fallarán y no nos enteraremos. Y si las hacemos síncronas y tardan más de unas centésimas de segundo puede que nunca lleguen al servidor.

Como puedes ver el tema tiene mucha "tela" y es muy complejo. Luego comentaré más detalles.

Por ese motivo, en general, es muy mala idea gestionar cosas del servidor desde el lado cliente, precisamente por estas complejidades y porque todo lo que esté en el lado cliente está fuera de nuestro control y puede ocurrir cualquier cosa.

Vale, ¿entonces qué podemos hacer con todas estas limitaciones?

Pues hay dos cosas mas que debemos conocer.

Por un lado existen los eventos de transición de página, que son un par de eventos del navegador que son especialmente útiles con navegadores móviles (pero que sirven en escritorio también): el evento pagehide y el evento pageshow. No hay mucha documentación actualizada sobre ellos (ni en MDN ni en casi ningún lado), pero funcionan y se pueden combinar con los otros como veremos en breve. Si lees la nota verde del primer enlace anterior:

Fragmento de texto del estándar que habla de problemas de estos eventos

verás que existen muchas excepciones y las que pone ahí (no normativas) no son ciertas en todos los navegadores. Pero en general puedes considerar que el evento pagehide saltará en los navegadores móviles "raros" (como el de Xiami o Samsung) cuando la página se "congele" al cambiar a una nueva, y también lo hace en Safari para iOS. Y pageshow saltará cuando se retome la página desde ese estado (pero no cuando cargue por primera vez, por ejemplo).

Por otro lado existe una alternativa al uso de AJAX para hacer llamadas informativas rápidas a los servidores que es la API de Beacon (se lee /bïcon/, no como el jamón ahumado 😆), que está pensada para hacer llamadas asíncronas rápidas al servidor sin esperar ni recibir respuesta, y que se pueden utilizar en el evento unload (están específicamente pensadas para esto). Para cierto de cosas es perfecto porque es tan solo una línea de código, disparas y te olvidas.

Entonces, ¿qué puedes hacer para asegurar que te funcionan ciertas llamadas en segundo plano cuando se descargue o desaparezca la página en un navegador móvil?

En primer lugar cruzar los dedos porque esto puede variar de un día para otro y hay muchas cosas raras por ahí, pero después lo que deberías hacer es:

  • Usar sendBeacon() en lugar de llamadas AJAX.
  • Capturar los tres eventos: pagehide, onbeforeunload y unload y tratar de hacer la llamada en todos ellos, porque según las circunstancias saltarán unos u otros (¡o todos!) y en orden diferente.
  • Establecer una variable global de bloqueo que por defecto valga false y que establezcas a true en todos los eventos anteriores para determinar si se ha podido realizar la llamada al servidor con éxito o no (o sea, si ha saltado cada evento o no) y que se compruebe en todos los eventos antes de nada para no lanzar la llamada si se ha hecho ya antes (o las recibiríamos varias veces en el servidor).

De esta manera lograrías tener controlado (o casi) el tema y hacer lo que necesitas, pero como ves es complicado y propenso a errores, y como te digo, mejor hacer este tipo de cosas en el servidor siempre que puedas y no delegarlas en el lado cliente (navegador).

Adjunto el ejemplo que he usado (test-beforeunload-JASoft.zip, 1KB), con los 3 eventos, para que veas cuando salta cada uno. Para verlo tienes que abrir las herramientas del desarrollador y marcar la opción de persistir el log, sino al cambiar de página perderías los mensajes:

Más notas sobre el comportamiento de estos eventos

Hacer cosas en estos eventos de descarga tiene bastante complejidad y puede variar de unas épocas a otras porque los navegadores van cambiando de opinión respecto a cómo hacer las cosas. Por ejemplo, en Chrome nunca has podido hacer llamadas AJAX asíncronas en el evento unload, pero podías hacer llamadas síncronas hasta que en la versión 30 (en octubre de 2013, ¡ya llovió!) dejaron de darle soporte y ahora no puedes hacer llamadas AJAX de tipo alguno en el evento unload. Sólo puedes usar los Beacon que acabo de explicar. En el beforeunload la cosa también varía según el navegador y de hecho aunque ahora te funcione puede que te deje de funcionar dentro de un mes. No lo puedes saber. Y de hecho, como ya he comentado, hay muchos navegadores en los que ni siquiera te llegan a saltar nunca.

Y es que esos eventos están pensados para limpiar recursos usados en la página o, en el caso del beforeunload, para evitar que la gente se salga de la página cuando tienen cosas sin guardar, y evitarles así perder los datos.

Aparte de todo esto hay un detalle adicional importante, y es que el evento beforeunload no salta en una página si el usuario no ha interactuado previamente con ella. Se ve en el GIF animado anterior, pero dejo aquí un detalle de la captura (hay que dejar activado el log persistente de las herramientas del desarrollador para verlo):

 

Es fácil obviar este detalle. Pero si funciona, lo hace aunque intentes cerrar "a saco" la pestaña, no sólo al navegar. Como ves, una nueva dificultad a considerar. Y es que, nuevamente, no tiene sentido que salte si el usuario no ha interactuado porque está pensado para evitar que cierre perdiendo información. Pero no para otras cosas.

Otro detalle: si cambias de domino y no solo navegas en el mismo dominio, si capturas el evento beforeunload puede que no te salte el evento unload (en Chrome por ejemplo) mientras que si es navegación en el mismo dominio sí...

Hay miles de detalles que tienes que probar de cada vez porque no son estándar y además pueden ir cambiando.

¡Espero que te resulte útil!

💪🏻 ¿Este post te ha ayudado?, ¿has aprendido algo nuevo?
Pues NO te pido que me invites a un café... Te pido algo más fácil y mucho mejor

Escrito por un humano, no por una IA