Browser Back/Forward Button History Navigation Cache (bfcache)

According to Chrome usage data, 10% of browser navigation on desktop and 20% on mobile are simple user initiated back and forth! Imagine the massive internet bandwidth usage or data transfer caused by all the backward and forward button clicks loading all kinds of static assets like html, css, javascript, images, fonts, etc.

This unnecessary massive data consumption has been cut down with the back/forward cache or bfcache browser optimization (available in all major browsers). The bfcache optimization enables significantly faster (almost instant) loading of webpages on desktop and mobile browsers via both:

  1. User initiated back and forward browser button clicks.
  2. The history.back/forward/go JavaScript web API calls.

It is super useful on slow networks and devices. Here’s a video demo that compares two experiences, one with bfcache enabled and the other without.

Implementation

If you’re worried that you have to learn a bunch of APIs and then write some code and manifests to get bfcache benefits, then relax. Caching involved in back/forward cache “just works” in majority of the cases because browsers automatically do all the work for that. Developers don’t need to do much in terms of writing code except a few optimizations here and there.

How does the browser achieve this ?

A bunch of operations or steps are involved behind putting a web page into the bfcache.

  1. A complete snapshot of the webpage is stored in-memory, including the JavaScript heap. This happens when for instance the user navigates from example.com/foo to example.com/bar (same site) or anotherexample.com (cross site).
  2. Processing of all tasks like timers, promises, etc. in the task queues are paused (when a new navigation request has to happen) and eventually resumed upon page restoration (when say the back button is clicked).
  3. In the following cases, caching is completely avoided to prevent any inconsistencies:
    • If the page has any open IndexedDB connections.
    • If the page has any in-progress fetch() or XMLHttpRequest calls.
    • If the page has any open WebSocket or WebRTC connections.

Improving the Cache-Hit Eligibility

Not everytime can bfcache kick in. For instance in the previous section we saw there are certain cases where bfcache is completely avoided. Apart from those, there are certain eligibility criteria as well:

  1. Using the unload event on a web page disables bfcache. Replace all your unload event usage with the pagehide event.
  2. Adding event listeners to beforeunload event also makes the page ineligible for bfcache in Firefox, but not in Chrome or Safari. There’s one use case where you may really have to use it which is preventing loss of any “unsaved changes”. For such cases, add conditional checks where the event listener is attached when some “unsaved change” is generated, instead of attaching them right away (on page load). So it’ll look something like this – if (haveChanges) { // attach listener } and not addEventListener('beforeunload', () => { // Action for haveChanges }. This way at least when there are no changes, bfcache will kick in for Firefox.
  3. A page with a non-null window.opener can’t be cached.

Relevant Events

The page transition events pageshow and pagehide are similar to load and unload/beforeunload events respectively, except that they’re compatible with bfcache and can be used to handle any side-effects or perform some cleanup activities (we’ll cover these concepts below). First, lets understand when are the page transition events called (irrespective of bfcache):

  • pageshow event is triggered on every page load, after the load event.
  • pagehide event is triggered on every page unload, after the beforeunload event but before unload event.

Now when a page loads or unloads, it will either involve bfcache where the page is retrieved from the cache and stored into the cache, or it will not involve any caching. If it does involve bfcache then the event listeners attached to the pageshow and pagehide events will receive an event object which will contain a persisted property set to true. If no bfcache is involved then this property will be set to false. The event object passed will be an instance of PageTransitionEvent which is a wrapper around normal Event objects.

So if event.persisted === true then:

  • In context of pageshow it means that the page is restored or fetched from bfcache.
  • In context of pagehide it means that the page will be stored into bfcache.

Where as if event.persisted === false then browser is loaded or unloaded normally without any bfcache in the picture. Examples of such cases are page reloads, pages not meeting eligibility criteria as listed above, open connections preventing browser from caching page snapshot, etc.

The fact that the event object passed to pageshow and pagehide event listeners contains a persisted property, makes these events super relevant to bfcache, unlike events like beforeunload, unload, load, etc.

What does compatibility mean in this case though ? As we saw earlier, due to certain reasons, browsers won’t trigger bfcache if you make use of unload or beforeunload events. This is not the case with pagehide, hence page transition events are said to be compatible with bfcache.

What does handle any side-effects or cleanup activity mean ?

  1. In case of open connections for APIs like IndexedDB, WebSockets, WebRTC, etc. we discussed earlier that the bfcache won’t kick in. We could take care of this by doing “cleanup” work in a pagehide event listener, i.e., closing connections to make the page becomes eligible for caching. Since the pagehide event is fired before the page is about to unload for upcoming navigation and before the browser does the actual caching work, if we close all the open connections here then the browser can safely decide to cache the entire page (including JS heap) in memory, before the actual navigation happens. Yay!
  2. Caching can lead to stale data. For example if a user performs logout and then clicks on back to come back to the previous page, the page will be fetched from the cache showing old data reflecting the user as logged in. In such cases pages you must perform a few checks and reload the page inside a pageshow event listener.
  3. Analytics or performance tracking functions won’t be called when the page is fetched from bfcache due to obvious reasons. Call the relevant functions to initiate tracking inside a pageshow event listener.

Here’s a sample boilerplate on how your listener to do cleanups or handle side-effects would look like:

window.addEventListener('pageshow|pagehide', (event) => {
  if (event.persisted) {
    // - Close connections
    // - Reload pages if any state change can be expected
    // - Trigger analytics pixels
    console.log('If pageshow - This page was restored from the bfcache.');
    console.log('If pagehide - This page *might* be entering the bfcache.');
  } else {
    console.log('If pageshow - This page was loaded normally.');
    console.log('If pagehide - This page will unload normally and be discarded.');
  }
});

There are a few other events from the page lifecycle APIresume and freeze – that fire when bfcache is restored (fetch) and stored (save) respectively. But they also fire in some other cases not specific to bfcache.

The newer Page Lifecycle events—freeze and resume—are also dispatched when pages go in or out of the bfcache, as well as in some other situations. For example when a background tab gets frozen to minimize CPU usage. Note, the Page Lifecycle events are currently only supported in Chromium-based browsers.

Dev Tooling

In Chrome dev tools, you can easily see if a webpage is optimized and eligible for bfcache or not under Applications > Cache > Back/forward cache.

Chrome Dev Tools bfcache

If your page is ineligible for caching, then dev tools will either suggest certain actionables like convert all your unload usages into pagehide. In some cases where the ineligibility is out of your control, i.e., you can’t really fix it, dev tools will just show you the reason without presenting any actionable. This may happen if it’s a browser bug which will be fixed in the future.

Leave a Reply

Your email address will not be published. Required fields are marked *