Back and forward cache

Back/forward cache (or bfcache) is a browser optimization that enables instant back and forward navigation. It significantly improves the browsing experience, especially for users with slower networks or devices.

This page outlines how to optimize your pages for bfcache across all browsers.

Browser compatibility

bfcache has been supported in both Firefox and Safari for many years, across desktop and mobile.

Starting in version 86, Chrome enabled bfcache for cross-site navigations on Android for a small percentage of users. In subsequent releases, additional support slowly rolled out. Since version 96, bfcache is enabled for all Chrome users across desktop and mobile.

bfcache basics

bfcache is an in-memory cache that stores a complete snapshot of a page as the user navigates away. With the entire page in memory, the browser can quickly restore it if the user decides to return, instead of needing to repeat all the network requests necessary to load the page.

The following video shows just how much bfcache can speed up navigation:

Using bfcache makes pages load much more quickly during back and forward navigation.

Chrome usage data shows that 1 in 10 navigations on desktop and 1 in 5 on mobile are either back or forward. Because of this, bfcache has the potential to save a great deal of time and data usage.

How the "cache" works

The "cache" used by bfcache is different from the HTTP cache, which plays its own role in speeding up repeat navigations. bfcache is a snapshot of the entire page in memory, including the JavaScript heap, whereas the HTTP cache contains only the responses for previously made requests. Because it's very rare for all requests required to load a page to be fulfullable from the HTTP cache, repeat visits using bfcache restores are always faster than even the best-optimized non-bfcache navigations.

Creating a snapshot of a page in memory, however, involves some complexity in terms of how best to preserve in-progress code. For example, how do you handle setTimeout() calls where the timeout is reached while the page is in the bfcache?

The answer is that browsers pause any pending timers or unresolved promises for pages in bfcache, including almost all pending tasks in the JavaScript task queues, and resume processing tasks if the page is restored from the bfcache.

In some cases, such as for timeouts and promises, this is fairly low risk, but in other cases it can lead to confusing or unexpected behavior. For example, if the browser pauses a task that's required for an IndexedDB transaction, it can affect other open tabs in the same origin, because the same IndexedDB databases can be accessed by multiple tabs simultaneously. As a result, browsers usually won't try to cache pages in the middle of an IndexedDB transaction or while using APIs that might affect other pages.

For more details on how API usage affects a page's bfcache eligibility, see Optimize your pages for bfcache.

The bfcache and Single Page Apps (SPA)

Because bfcache works with browser-managed navigations, it doesn't work for "soft navigations" within a single-page app (SPA). However, bfcache can still help when leaving and returning to an SPA.

APIs to observe bfcache

Although bfcache is an optimization that browsers do automatically, it's still important for developers to know when it's happening so they can optimize their pages for it and adjust any metrics or performance measurement accordingly.

The primary events used to observe bfcache are the page transition events pageshow and pagehide, which are supported by most browsers.

The newer Page Lifecycle events, freeze and resume, are also dispatched when pages enter or leave bfcache, as well as in some other situations, for example, when a background tab gets frozen to minimize CPU usage. These events are only supported in Chromium-based browsers.

Observe when a page is restored from bfcache

The pageshow event fires right after the load event when the page is initially loading and any time the page is restored from bfcache. The pageshow event has a persisted property, which is true if the page was restored from bfcache and false otherwise. You can use the persisted property to distinguish regular page loads from bfcache restores. For example:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    console.log('This page was restored from the bfcache.');
  } else {
    console.log('This page was loaded normally.');
  }
});

In browsers that support the Page Lifecycle API, the resume event fires when pages are restored from bfcache (immediately before the pageshow event) and when a user revisits a frozen background tab. If you want to update a page's state after it's frozen (which includes pages in the bfcache), you can use the resume event, but if you want to measure your site's bfcache hit rate, you'd need to use the pageshow event. In some cases, you might need to use both.

For details on bfcache measurement best practices, see How bfcache affects analytics and performance measurement.

Observe when a page is entering bfcache

The pagehide event fires either when a page unloads or when the browser tries to put it in the bfcache.

The pagehide event also has a persisted property. If it's false, you can be confident a that page isn't about to enter the bfcache. However, persisted being true doesn't guarantee that a page will be cached. It means the browser intends to cache the page, but there may be other factors that make it impossible to cache.

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    console.log('This page *might* be entering the bfcache.');
  } else {
    console.log('This page will unload normally and be discarded.');
  }
});

Similarly, the freeze event fires immediately after the pagehide event if persisted is true, but that only means the browser intends to cache the page. It might still have to discard it for a number of reasons explained later.

Optimize your pages for bfcache

Not all pages get stored in bfcache, and even when a page does get stored there, it won't stay there indefinitely. The following pages outline what make pages eligible for bfcache, and recommends best practices to maximize the browser's ability to cache your page for better cache-hit rates.

Use pagehide instead of unload

The most important way to optimize for bfcache in all browsers is to never use unload event listeners. Listen for pagehide instead, because it fires both when a page enters bfcache and any time unload fires.

unload is an older feature, originally designed to trigger any time a user navigated away from a page. This is no longer the case, but many web pages still operate on the assumption that browsers use unload in this way, and that after unload triggers, the unloaded page stops existing. This has the potential to break bfcache if the browser tries to cache an unloaded page.

On desktop, Chrome and Firefox make pages with unload listeners ineligible for bfcache, which reduces risk but also causes a lot of pages to not be cached and therefore reload much slower. Safari does try to cache some pages with unload event listeners, but to reduce potential breakage, it doesn't run the unload event when a user navigates away, which makes unload listeners unreliable.

On mobile, Chrome and Safari try to cache pages with unload event listeners, because unload's unreliability on mobile makes the risk of breakage lower. Mobile Firefox treats pages that use unload as ineligible for the bfcache, except on iOS, which requires all browsers to use the WebKit rendering engine, so it behaves like Safari.

To determine whether any JavaScript on your pages uses unload, we recommend using the no-unload-listeners audit in Lighthouse.

For information on Chrome's plan to deprecate unload, refer to Deprecating the unload event.

Use Permission Policy to prevent unload handlers being used on a page

Some third-party scripts and extensions can add unload handlers to a page, slowing the site down by making it ineligible for bfcache. To prevent this in Chrome 115 and later, use a Permissions Policy.

Permission-Policy: unload()

Only add beforeunload listeners conditionally

The beforeunload event doesn't make your pages ineligible for bfcache. However, it's unreliable, so we recommend only using it when it's absolutely necessary.

An example use case for beforeunload is warning a user that they have unsaved changes they'll lose if they leave the page. In this case, we recommend adding beforeunload listeners only when a user has unsaved changes, and then removing them immediately after the unsaved changes are saved, as in the following code:

function beforeUnloadListener(event) {
  event.preventDefault();
  return event.returnValue = 'Are you sure you want to exit?';
};

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
  window.addEventListener('beforeunload', beforeUnloadListener);
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
  window.removeEventListener('beforeunload', beforeUnloadListener);
});

Minimize use of Cache-Control: no-store

Cache-Control: no-store is an HTTP header web servers can set on responses that instructs the browser not to store the response in any HTTP cache. It's used for resources containing sensitive user information, such as pages behind a login.

Although bfcache isn't an HTTP cache, browsers have historically excluded pages from bfcache when Cache-Control: no-store is set on the page resource (but not on any subresource). Chrome is working on changing this behavior while maintaining user privacy, but by default, pages using Cache-Control: no-store aren't eligible for bfcache.

To optimize for bfcache, use Cache-Control: no-store only on pages containing sensitive information that must not be cached.

For pages that want to always serve up-to-date content, but don't include sensitive information, use Cache-Control: no-cache or Cache-Control: max-age=0. These tell the browser to revalidate the content before serving it, and they don't affect a page's bfcache eligibility because restoring a page from bfcache doesn't involve the HTTP cache.

If your content changes minute-by-minute, fetch updates using the pageshow event instead to keep your page up to date as described in the next section.

Update stale or sensitive data after bfcache restore

If your site keeps user state data, and especially if that data includes sensitive user information, it must be updated or cleared after a page is restored from bfcache.

For example, if a user signs out of a site on a public computer and the next user clicks the back button, the stale data from bfcache might include private data that the first user expected to be cleared when they logged out.

To avoid situations like this, always update the page after a pageshow event if event.persisted is true:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Do any checks and updates to the page
  }
});

For some changes, you might want to force a full reload instead, preserving navigation history for forward navigations. The following code checks for the presence of a site-specific cookie in the pageshow event and reloads if the cookie isn't found:

window.addEventListener('pageshow', (event) => {
  if (event.persisted && !document.cookie.match(/my-cookie)) {
    // Force a reload if the user has logged out.
    location.reload();
  }
});

Ads and bfcache restore

It can be tempting to try to avoid using bfcache so your page can serve a new set of ads on each back or forward navigation. However, this is bad for site performance, and doesn't consistently increase ad engagement. For example, a user might intend to return to a page to click an ad, but if the page is reloaded instead of restored from bfcache, it might show a different ad. We recommend using A/B testing to determine the best strategy for your page.

For sites that want to refresh ads on bfcache restore, you can refresh only the ads on the pageshow event when event.persisted is true without impacting the page performance, as in this Google Publishing Tag example. For more information about best practices for your site, check with your ad provider.

Avoid window.opener references

In older browsers, if a page was opened using window.open() from a link with target=_blank, without specifying rel="noopener", the opening page would have a reference to the window object of the opened page.

In addition to being a security risk, a page with a non-null window.opener reference can't safely be put into bfcache, because it might break any pages that try to access it.

To avoid these risks, use rel="noopener" to prevent the creation of window.opener references. This is the default behavior in all modern browsers. If your site needs to open a window and control it using window.postMessage() or by directly referencing the window object, neither the opened window nor the opener are eligible for bfcache.

Close open connections before the user navigates away

As mentioned previously, when a page is put into bfcache, it pauses all scheduled JavaScript tasks and resumes them when the page is taken out of the cache.

If these scheduled JavaScript tasks access only DOM APIs, or other APIs isolated to the current page, pausing these tasks while the page isn't visible to the user doesn't cause problems.

However, if these tasks are connected to APIs that are also accessible from other pages in the same origin (for example: IndexedDB, Web Locks, and WebSockets), pausing them can break those pages by preventing code on those pages from running.

As a result, some browsers won't try to put a page in bfcache if it has one of the following:

If your page is using any of these APIs, we strongly recommend closing connections and removing or disconnecting observers during the pagehide or freeze event. That allows the browser to safely cache the page without the risk of affecting other open tabs. Then, if the page is restored from the bfcache, you can reopen or reconnect to those APIs during the pageshow or resume event.

The following example shows how to ensure that pages using IndexedDB are eligible for bfcache by closing an open connection in the pagehide event listener:

let dbPromise;
function openDB() {
  if (!dbPromise) {
    dbPromise = new Promise((resolve, reject) => {
      const req = indexedDB.open('my-db', 1);
      req.onupgradeneeded = () => req.result.createObjectStore('keyval');
      req.onerror = () => reject(req.error);
      req.onsuccess = () => resolve(req.result);
    });
  }
  return dbPromise;
}

// Close the connection to the database when the user leaves.
window.addEventListener('pagehide', () => {
  if (dbPromise) {
    dbPromise.then(db => db.close());
    dbPromise = null;
  }
});

// Open the connection when the page is loaded or restored from bfcache.
window.addEventListener('pageshow', () => openDB());

Test to ensure your pages are cacheable

Chrome DevTools can help you test your pages to ensure they're optimized for bfcache, and identify any issues that might prevent them from being eligible.

To test a page:

  1. Navigate to the page in Chrome.
  2. In DevTools, go to Application > Back-forward Cache.
  3. Click the Run Test button. DevTools then tries to navigate away and back to determine whether the page can be restored from bfcache.
Back-forward cache panel in DevTools
The Back-forward Cache panel in DevTools.

If the test is successful, the panel reports "Restored from back-forward cache". If it's unsuccessful, the panel indicates the reason why. For a full list of reasons, see Not restored reasons list for Chromium.

If the reason is something you can address as a developer, the panel marks it as Actionable.

DevTools reporting failure to restore a page from bfcache
A failed bfcache test with an actionable result.

In this image, the use of an unload event listener makes the page ineligible for bfcache. You can fix that by switching from unload to using pagehide:

Do
window.addEventListener('pagehide', ...);
Don't
window.addEventListener('unload', ...);

Lighthouse 10.0 also added a bfcache audit, which performs a similar test. For more information, see the bfcache audit's docs.

How bfcache affects analytics and performance measurement

If you use an analytics tool to track visits to your site, you might notice a decrease in the total number of pageviews reported as Chrome enables bfcache for more users.

In fact, you're likely already underreporting pageviews from other browsers that implement bfcache, because most popular analytics libraries don't track bfcache restores as new pageviews.

To include bfcache restores in your pageview count, set listeners for the pageshow event and check the persisted property.

The following example shows how to do this with Google Analytics. Other analytics tools likely use similar logic:

// Send a pageview when the page is first loaded.
gtag('event', 'page_view');

window.addEventListener('pageshow', (event) => {
  // Send another pageview if the page is restored from bfcache.
  if (event.persisted) {
    gtag('event', 'page_view');
  }
});

Measure your bfcache hit ratio

To identify pages that don't yet use bfcache, measure the navigation type for page loads as follows:

// Send a navigation_type when the page is first loaded.
gtag('event', 'page_view', {
   'navigation_type': performance.getEntriesByType('navigation')[0].type;
});

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Send another pageview if the page is restored from bfcache.
    gtag('event', 'page_view', {
      'navigation_type': 'back_forward_cache';
    });
  }
});

Calculate your bfcache hit ratio using the counts for back_forward navigations and back_forward_cache navigations.

Reasons a back or forward navigation might not use bfcache include the following user behavior:

  • Quitting and restarting the browser.
  • Duplicating a tab.
  • Closing and restoring a tab.

In some of these cases, the browser might preserve the original navigation type and show a type of back_forward despite these not being back or forward navigations. Even when navigation types are reported correctly, the bfcache is periodically discarded to save memory.

Because of this, website owners can't expect a 100% bfcache hit ratio for all back_forward navigations. However, measuring their ratio can help identify pages that prevent bfcache usage.

The Chrome team is working on a NotRestoredReasons API to help expose the reasons why pages don't use bfcache, so developers can improve their bfcache hit rates.

Performance measurement

bfcache can also negatively affect performance metrics collected in the field, specifically metrics that measure page load times.

Because bfcache navigations restore an existing page, instead of starting a new page load, the total number of page loads collected decreases when bfcache is enabled. However, the page loads bfcache replaces were likely among the fastest page loads in your dataset, because repeat page loads, including back and forward navigations, and usually faster than first-time page loads because of HTTP caching. So enabling bfcache can cause your analytics to show slower page loading, in spite of improving site performance for the user.

There are a few ways to deal with this issue. One is to annotate all page load metrics with their respective navigation type: navigate, reload, back_forward, or prerender. This lets you continue to monitor your performance within these navigation types, even if the overall distribution skews negative. We recommend this approach for non-user-centric page load metrics like Time to First Byte (TTFB).

For user-centric metrics like the Core Web Vitals, a better option is to report a value that more accurately represents what the user experiences.

Impact on Core Web Vitals

Core Web Vitals measure the user's experience of a web page across a variety of dimensions (loading speed, interactivity, visual stability). It's important that your Core Web Vitals metrics reflect the fact that users experience bfcache restores as faster navigations than default page loads.

Tools that collect and report on the Core Web Vitals metrics, like the Chrome User Experience Report, treat bfcache restores as separate page visits in their dataset. And while there aren't dedicated web performance APIs for measuring these metrics after bfcache restores, you can approximate their values using existing web APIs:

  • For Largest Contentful Paint (LCP), use the delta between the pageshow event's timestamp and the timestamp of the next painted frame, because all elements in the frame will be painted at the same time. In the case of a bfcache restore, LCP and FCP are the same.
  • For Interaction to Next Paint (INP), keep using your existing Performance Observer, but reset the current CLS value to 0.
  • For Cumulative Layout Shift (CLS), keep using your existing Performance Observer, but reset the current CLS value to 0.

For more details on how bfcache affects each metric, see the individual Core Web Vitals metric guides pages. For a specific example of how to implement bfcache versions of these metrics, refer to the PR adding them to the web-vitals JS library.

Additional Resources