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:
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 iframes
If a page contains embedded iframes, then the iframes themselves are not eligible for the bfcache. For example, if you navigate to another page within an iframe, but then go back, the browser will go "back" within the iframe rather than in the main frame, but the back navigation within the iframe won't use the bfcache.
The main frame can also be blocked from using the bfcache if an embedded iframe uses APIs that block this. The Permissions Policy set on the main frame or the use of sandbox
attributes can be used to avoid this.
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:
- An open IndexedDB connection
- In-progress fetch() or XMLHttpRequest
- An open WebSocket or WebRTC connection
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:
- Navigate to the page in Chrome.
- In DevTools, go to Application > Back-forward Cache.
- Click the Run Test button. DevTools then tries to navigate away and back to determine whether the page can be restored from bfcache.
If the test is successful, the panel reports "Restored from back-forward cache". If it's unsuccessful, the panel indicates the reason why.
If the reason is something you can address as a developer, the panel marks it as Actionable.
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
:
window.addEventListener('pagehide', ...);
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 has added the
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
- Firefox Caching (bfcache in Firefox)
- Page Cache (bfcache in Safari)
- Back/forward cache: web exposed behavior (bfcache differences across browsers)
- bfcache tester (test how different APIs and events affect bfcache in browsers)
- Performance Game Changer: Browser Back/Forward Cache (a case study from Smashing Magazine showing dramatic Core Web Vitals improvements by enabling bfcache)