How To Make Sense Of Page Events In Your Web App
Modern Web Weekly #36
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 withevent.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 thepagehide
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 closedpageshow
: 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 checkdocument.visibilityState
for its state (visible
orhidden
). 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:
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
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