The 3 things you need to know before adding support to BFCache in your site

DV Engineering
DoubleVerify Engineering
6 min readDec 20, 2023

Written By: Bar Yochai Bokovza

Imagine this scenario: You’re developing a website on a single browser. You validate that the site behaves properly, but your users are using other browsers. You decide to add support for more browsers, but each has its own customization.

This is a scenario that lots of web developers deal with every day. Back at the end of the 20th century, this was a huge problem, but today, there are standards in browser APIs.

But (and it’s a big but), we don’t ask ourselves if these APIs behave the same. We’re not talking about the interface of the APIs, or about the direct results of using them, but how they are implemented under the hood. For example:

  1. Will the browser fire an event?
  2. Will the browser optimize/not optimize the page in such a way that, as a developer, I might need to pay more for traffic?
  3. Will the browser eliminate data that the API should return?

In this post, I will show you 2 cases regarding listening to events and Back/Forward caching that show even when the API is the same, the behavior is completely different.

Webpage’s lifecycle events

One of the key APIs a browser exposes for developers is related to the state of the page when the user interacts with it:

  1. Is the user interacting with the page?
  2. Are they interacting with another page or tab, although the current page is still visible on the screen?
  3. Are they finished working with this page?

For all of those questions, we have lifecycle events. These are events that the browser fires when there’s a change in the state of the page related to the user:

  • Focus — When the user is interacting with the page.
  • Blur — When the user uses another tab or window on their desktop.
  • Visibility change — When there’s a change in the page’s visibility.

The whole lifecycle of the browser’s page can be seen in the diagram below:

The page events we will focus on in this post are:

  1. unload — Fires when the user leaves the page.
  2. beforeunload — Event that we can listen to to validate that the user wants to leave the page. Think about it as an “Are you sure?” event.
  3. load — Fires when the page is loaded to the screen.
  4. pageshow & pagehide — Similar to the load & unload event.

Now, you’re probably asking yourself, “Why do we have a pagehide event if there’s an unload event? Why do we need 2 events for the same incident?” To understand why, we need to consider a mechanism in the browser called Back/Forward caching.

Back/Forward caching

Back/Forward caching (aka BFCache, or Safari’s Page Cache) is a mechanism inside the browser that:

  1. Triggers when the user navigates to a different page (“Navigate Back” or new URL).
  2. Saves the current state of the page in the browser’s memory:
    - All parsed & rendered assets of the page (HTML, CSS, JS, images, videos, etc.).
    - Current page’s JS memory.
  3. Loads the page from the memory instantly when the user navigates “forwards” or back to the page.

Therefore, the browser:

  1. Reduces traffic consumption since we don’t request all of the assets again.
  2. Reduces CPU time because we don’t need to parse, render and load assets.
  3. Has better UX since the page loads instantly.

Why are pagehide and pageshow created?

Currently, in most browsers, BFCache is always turned on unless a condition is being fulfilled that makes the browser turn off the BFCache for the page. The “sanity check” that a browser does checks if the browser can freeze the current state of the page.

BFCache has broken the assumption the unload event is based on — the event is being fired when we know for sure the page will terminate right after that. Browser makers needed to solve this conflict and moreover, browsers are also in use on mobile, requiring optimized battery consumption. We can’t keep the process of a browser always running.

Therefore, pagehide and pageshow events are created, and we can assume they are always in the junction between page termination and page caching. Using them is also a recommendation by browser manufacturers.

How do we turn on BFCache?

Imagine you have a basic webpage with setTimeout. Where should the timeout fire if we freeze the page? Should it include the freeze period or not?

A common browser declares these conditions will block the page from entering the BFCache:

  1. Listening to unload and beforeunload events.
    - It’s highly recommended to listen to beforeunload conditionally and remove the listener once this event is fired.
  2. The page contains an asset with the Cache-Control: no-cache header. Use this when your page contains critical data, such as a bank account balance.
  3. Open WebSocket connection.
  4. An IndexedDB transaction that hasn’t finished.

There are more reasons that BFCache could be blocked, but these conditions will cover most of the cases.

Differences in caching policies

The main API, the user, can check if the page is cached or not via pagehide and pageshow events, via the persisted parameter given in these events. Although this API is the same across all browsers, we could get a different persisted value across different browsers.

Each browser has its own caching policy, which includes all the conditions that will cache or not cache the page to the BFCache. In the table below, we can see the difference between different browser caching policies.

If we have a page with a listener to an unload event, Safari will cache the page, while Chromium browsers (such as Google Chrome, Microsoft Edge, Brave, etc.) or Firefox will not. If you think that is the only difference between browsers related to unload event, this is just half the story.

Unload events

In our experiments, adding the caching mechanism showed that these events (pagehide, pageshow, unload, and beforeunload) fire at different rates between different browsers. This is another case of the same API across different browsers, but the behavior varies between them.

At DoubleVerify, we checked the differences between the invocation rate of pagehide, compared to the invocation rate of an unload event, and here are the conclusions:

  1. Across all browsers, we can see a positive difference — pagehide fires more than unload events.
  2. In some browsers, the unload invocation rate didn’t pass 50%.
    - The process of the page was killed before the unload event fired.
  3. In Safari, preferably listen to pagehide and not to unload, since the difference between the rate is ~2x.
    - Apple decided to cache pages that listen to unload events but don’t guarantee that they will fire this event, even if the page is not cached.

Therefore, if you have to catch the unload of the page:

  1. Listen to a pagehide event instead.
  2. On mobile, listen to visibilitychange events, since the process is halted when closing the browser app, and even then, pagehide won’t be fired.

Lifecycle events recommendations

In this article, we’ve shown cases when all browsers have the same APIs, but the engines behave differently in caching and firing events.

Therefore, we recommend:

  1. If you have a listener to unload event, remove it and listen instead to pagehide. Not only does this event gets invoked more than an unload event, it’ll notify you when the page is being cached.
  2. If you want to detect “page unloading” on a mobile device, just add a listener to visibilitychange event. This will make sure that the process of the browser’s app isn’t killed too soon.
  3. If you listen to a beforeunload event, remove the listener immediately after you know you no longer need it. Not removing the listener can block caching for Firefox.

--

--

DV Engineering
DoubleVerify Engineering

DoubleVerify engineers, data scientists and analysts write about their work and share their experience