How To Make Sense Of Page Events In Your Web App

How To Make Sense Of Page Events In Your Web App
Photo by Miguel Tomás / Unsplash

Modern Web Weekly #36

👋
Hello there! I'm Danny and this is Modern Web Weekly, a weekly update on Progressive Web Apps (PWA), Web Components and new features of the modern web platform, tested and explained in plain English.

Hide and seek: page events in your web app

Do you know the difference between the pagehide, beforeunload and visibilitychange event?

If you don't, I don't blame you.

The Page Visibility and Page Lifecycle APIs fire a series of events whenever a page is loaded or reloaded, when you navigate to another page, or when you close your web app. There are quite a lot of events that fire in different situations and on different platforms, so you'll be forgiven if you don't remember the details of each one.

The good news is that I tested this for you in different browsers on mobile and desktop and here's what I learned:

  • different events fire on different platforms
  • browsers fire different events
  • different events fire depending on whether or not the web app is installed.

Let's first have a look at all the possible events that are fired:

  • beforeunload: fired when the current document is about to be unloaded. When it fires, the document is still visible and can still be canceled with event.preventDefault. This event is often used to alert a user if they have unsaved changes somewhere and prevents the document from being reloaded or closed. While it's still widely supported, using it is discouraged in favor of the pagehide event. This event is fired reliably in Chrome and Safari on desktop but not on mobile as we'll see later.
  • pagehide: fired when the browser hides the current document to show a new document, reload the current document, or when the tab/browser is closed
  • pageshow: fired when the browser shows a (new) document due to navigation (navigating to a new page or reloading a page)
  • pageswap:  fired before the last frame of a page is rendered. This event is used with cross-document view transitions. You can use this for last-minute changes to the outgoing page, right before the old snapshots get taken.
  • pagereveal: fired before the first frame of the page is rendered. This event is used with cross-document view transitions. You can use this for last-minute changes to the new page, right before the new snapshots get taken.
  • visibilitychange: fired when the document becomes visible or is hidden. When the event is fired, you can check document.visibilityState for its state (visible or hidden). You can use this event to pause media or animations that don't need to be played when the user is not looking at the page.

Currently, pagereveal and pageswap are only supported in Chrome/Edge and Safari Tech Preview.

Let's take a closer look at which events are fired and when in different browsers and platforms.

Chrome on desktop
When you close a web app in Chrome on desktop, the page fires the following events:

beforeunload -> pagehide -> visibilitychange (state: hidden)

When you open the web app again, it fires these events:

pageshow -> visibilitychange (state: visible) -> pagereveal

When you navigate to another page, the old page fires:

beforeunload -> pagehide -> visibilitychange (state: hidden)

and the new page fires:

pageshow

When you reload a page, it fires:

beforeunload -> pageswap -> pagehide -> visibilitychange (state: hidden) -> pagereveal (sometimes) -> pageshow

In my testing, I noticed that the pagereveal event is only fired sometimes and not reliably.

Chrome on Android:
When you close a web app in Chrome on Android, it only fires the pagehide event when the web app is installed on the home screen. Otherwise, no event is fired when the app is closed.

When you open a web app, it only fires the pageshow event.

When you reload a page, it fires:

beforeunload -> pageswap -> pagehide -> visibilitychange (state: hidden) -> pageshow

Note that while in desktop Chrome the pagereveal event is fired sporadically, in Chrome on mobile this is not the case.

When you navigate to another page, the old page fires:

beforeunload -> pagehide -> visibilitychange (state: hidden)

and the new page fires:

pageshow

When you switch apps with the Android app switcher, the apps you switch between only fire visibilitychange events with the states hidden and visible.

When you open the app switcher, the active app immediately fires a visibilitychange event with the state hidden as soon as you open the switcher.

Safari on desktop
When you close a web app in Safari on desktop, the page fires the same events as in Chrome:

beforeunload -> pagehide -> visibilitychange (state: hidden)

When you open the web app again, it fires these events:

pageshow -> visibilitychange (state: visible)

Unlike Chrome on desktop, Safari doesn't fire the pagereveal event when a web app is opened but Safari Tech Preview does, which signals support is upcoming.

When you navigate to another page, the old page fires:

beforeunload -> pagehide -> visibilitychange (state: hidden)

and the new page fires:

pageshow

When you reload a page, it fires:

beforeunload -> pagehide -> visibilitychange (state: hidden) -> pageshow

Again, unlike Chrome on desktop, Safari doesn't fire the pageswap event. However, Safari Tech Preview does and it also fires the pagereveal event so the flow looks like this:

beforeunload -> pageswap -> pagehide -> visibilitychange (state: hidden) -> pagereveal ->pageshow

Safari on iOS
A web app in Safari on iOS that is closed fires these events:

pagehide -> visibilitychange (state: hidden)

and when it's opened it only fires the pageshow event. Like in Chrome on Android, the pagehide event is only fired when the web app is installed on the home screen and the same is true for visibilitychange. So when a web app running in Safari on iOS is closed, no event is fired if the web app is not installed to the home screen.

When you navigate to another page, the old page fires:

pagehide -> visibilitychange (state: hidden)

and the new page fires:

pageshow

When you reload a page, it fires:

pagehide -> visibilitychange (state: hidden) -> pageshow

When switching apps with the iOS app switcher, the apps you switch between fire visibilitychange events with the states hidden and visible.

Unlike Android where a visibilitychange event with state hidden is fired as soon as the switcher is opened, iOS only fires a visibilitychange event with state hidden when the web app is moved to the background because the user focuses another app.

On both Android and iOS, a pagehide event is not fired when an app is moved to the background and then closed later. So when someone uses your web app, moves it to the background with the app switcher, starts using another app, and then closes your web app later, you unfortunately have no way to detect if your app was closed.

What to conclude from all this

Although Page Visibility and Page Lifecycle seem like daunting and complex APIs, the good news is that the flow on desktop is almost identical between Chrome and Safari. The difference is the implementation of the pageswap and pagereveal events that are not yet supported in the latest Safari but as mentioned, they are in Safari Tech Preview so support is upcoming.

On mobile, support is varying and less consistent. The pagehide event is only fired for web apps that are installed when closed and neither Android nor iOS fire the pagehide event for apps that are closed while they're already in the background.

Chrome also fires the beforeunload event when reloading or navigating away from a page but not when the web app is closed. Safari on iOS doesn't support pageswap and pagereveal yet either.

If you rely on these event to determine if you need to save state or perform some other action it will take some careful orchestration to create a reliable flow on mobile and desktop.

I use these events in my basic Service Worker project to signal to any waiting Service Worker that it can be activated on the next page load. If you need something like that you can use this project or study its source code:

GitHub - DannyMoerkerke/basic-service-worker: A basic Service Worker to make your web app work offline
A basic Service Worker to make your web app work offline - DannyMoerkerke/basic-service-worker

Mastering Web Components

Mastering Web Components is a course that will take you from beginner to expert in Web Components by teaching you how can create your own reusable Web Components and integrate them into any web app.

In the full, paid version of the course, you will build an image gallery component to apply and test your knowledge of the concepts you have learned.

The course contains many interactive code examples you can study and modify to test and sharpen your skills.

In the free version, you will learn:

  • how to create and register a Web Component
  • how to effectively use the lifecycle methods of Web Components
  • how to encapsulate the HTML and CSS of your Web Component using Shadow DOM
  • how to extend native HTML elements
  • how to compose Web Components with user-defined content
  • how to test Web Components

You get:

  • 107 page PDF
  • 22 interactive code examples

The full version includes everything from the free version and you will also learn:

  • how to theme and share styling between Web Components
  • how to integrate Web Components into forms and validate them
  • how to server-side render Web Components
  • how to implement data-binding for Web Components
  • how to compose Web Components using the mixin pattern
  • how to build Web Components with a library

You get:

  • 257 page PDF
  • 45+ interactive code examples
Become a Web Components expert!

AirPlay not working in Safari when the source of the <video> is a stream

I found that AirPlay doesn't work on iOS and macOS when the source of the video is a stream (like the stream from your webcam) or an objectURL.

I created a test page so you can reproduce it as well: https://whatpwacando.today/airplay

Click the plus button in the bottom left corner of the webcam component to start your webcam and then click/tap the AirPlay button.

Select an available AirPlay-capable device and you'll notice it won't work.

Now click the record button to record some video from your webcam. Stop recording by clicking the stop button, click the play button to play the recording and then the AirPlay button. This won't work either.

On my AppleTV, any video that was playing is stopped but the stream from the camera and the recording are not displayed.
This is similar to the bug about picture-in-picture that doesn't work either when the source is a stream and the web app is added to the Home Screen.

These are the relevant Webkit bug reports:

https://bugs.webkit.org/show_bug.cgi?id=281693
https://bugs.webkit.org/show_bug.cgi?id=262479