TL;DR
There is one approach to receive or emulate beforeunloadcancelled event in browsers using only browser APIs right now. But it doesn't work due to bugs in browsers which still haven't been fixed
Below I'll describe the main approach, why it doesn't work and what workarounds may be.
Approach based on browser APIs only
The main idea of emulating beforeunloadcancelled event is to get event that navigation request for next page has been cancelled by the user. You cannot control such kind of requests in the main browser context but, fortunately, you can do so from the service worker context.
If your service worker subscribes to fetch event, you will be able to work with all navigation requests your application makes:
// in Service Worker global scope
self.addEventListener('fetch', evt => {
if (evt.request.mode === 'navigate') {
console.log(`browser is navigating to the ${evt.request.url} resource now`);
}
});
According to the specification each Request object has property signal. This signal represents a signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object.
You can subscribe to the abort event to get notified when request is aborted by the browser or via an AbortController. In that listener you can post message to the parent window with the information that navigation request has been canceled which actually means beforeunloadcancelled event:
// in Service Worker global scope
self.addEventListener('fetch', evt => {
if (evt.request.mode === 'navigate') {
console.log(`browser is navigating to the ${evt.request.url} resource now`);
evt.request.signal.addEventListener('abort', async () => {
// exit early if we don't have an access to the client (e.g. if it's cross-origin)
if (!evt.clientId) return;
// get the client
const client = await self.clients.get(evt.clientId);
// exit early if we don't get the client (e.g., if it's closed)
if (!client) return;
// send a message to the client
client.postMessage({
msg: 'navigation request has been canceled'
});
})
}
});
Unfortunately, there are bugs that those abort events aren't reflected in service worker. Neither Chromium or Firefox have implemented properly sending the AbortSignal through to FetchEvent.request.signal. Please take a look:
So this approach doesn't work at the moment.
You can find more information on that topic here:
Possible workarounds
Detect of cancelled requests on the server side
If you have access to the server, you can detect that the navigation request has been cancelled on the server side.
Just send a notification to the client side in the event the navigation request is aborted. You can use WebSockets to send notifications. To make sure the notification is sent to the correct client some unique cookies based client Id may be used.
Timers based approaches
As you mentioned in the question, one possible workaround is to use some kind of timers:
window.addEventListener('beforeunload', () => {
showPreloader();
setTimeout(() => {
hidePreloader(); // we're assuming navigation has been cancelled
}, TIMEOUT);
});
To make this approach more precise you can choose the TIMEOUT value based on the current page load speed (which can be found in PerformanceNavigationTiming API):
const [initialNavigation] = window.performance.getEntriesByType('navigation');
const TIMEOUT = initialNavigation.duration * TIMEOUT_RATIO;
Track navigation request load duration with service worker
There is a way to track navigation request progress via service worker. You can send notification to the parent window when the navigation request is done. If navigation does not occur immediately after this notification, it means that navigation is effectively canceled and the preloader can be hidden:
// in Service Worker global scope
self.addEventListener('fetch', evt => {
if (evt.request.mode === 'navigate') {
console.log(`browser is navigating to the ${evt.request.url} resource now`);
evt.respondWith(fetch(evt.request).then(response => {
sendHidePreloaderNotification();
return response;
}));
}
});
You can always combine last two options developing more stable approach.