Enables back/forward cache (bfcache) for instant history navigations even when “nocache” headers are sent, such as when a user is logged in.
Contributors: westonruter, wordpressdotorg, performanceteam
Tags: performance, caching
Tested up to: 6.8
Stable tag: 1.3.1
License: GPLv2 or later
This plugin was formerly called as “No-cache BFCache”, which admittedly was a poor jargony name and was too narrowly scoped.
This plugin enables instant back/forward navigation via the browser's bfcache. It does this by removing the no-store
directive from the Cache-Control
response header, which WordPress sends by default when nocache_headers()
is called. This happens primarily when a user is logged in, but some plugins may send these “no-cache” headers such as on the Cart or Checkout pages for an e-commerce site. Upon activation, to see the effect, you must log out of WordPress and log back in again, ensuring “Remember Me” is checked. Even so, another plugin, theme or server configuration may be active which makes pages ineligible for bfcache due to other blocking reasons. Nevertheless, the removal of no-store
will still speed up back/forward navigations since pages may then be served from the browser's HTTP cache, eliminating the need to re-download the HTML from the server. This is a feature plugin to implement #63636 in WordPress core.
Blog post: Instant Back/Forward Navigations in WordPress
The speed of page navigations in WordPress saw a big boost in 6.8 with the introduction of Speculative Loading. However, by default Speculative Loading in WordPress is not configured to enable instant page loads, which requires a non-conservative eagerness with the prerender mode; not all sites can even opt in to prerendering due to compatibility issues, such as with analytics, and due to concerns about sustainability with unused prerenders (e.g. increasing server load and taxing a user's bandwidth/CPU). While Speculative Loading (i.e. the Speculation Rules API) is relatively new and currently only supported in Chromium browsers (e.g. Chrome and Edge), there is a much older web platform technology that enables prerendering and which is supported in all browsers: the back/forward cache (bfcache). This instant loading involves no network traffic and no CPU load, since previously visited pages are stored in memory. According to the web.dev article on back/forward cache:
Chrome usage data shows that 1 in 10 navigations on desktop and 1 in 5 on mobile are either back or forward. With bfcache enabled, browsers could eliminate the data transfer and time spent loading for billions of web pages every single day!
Also learn more via the following video:
Normally, WordPress sends a Cache-Control
header with the no-store
directive when a user is logged in. This has the effect of breaking the browser's bfcache, which means that navigating back or forward in the browser requires the pages to be re-fetched from the server and for any JavaScript on the page to re-execute. The result can be a sluggish navigation experience not only when navigating around the WP Admin (see Jetpack demo video and demo video below) but also when navigating around the frontend of a site. Furthermore, the lack of bfcache can cause data loss when data has been entered via a JavaScript-built UI since this state is lost when a page is not restored via bfcache (see WooCommerce demo video and demo video below).
The reason why the no-store
directive was added in the first place was due to a privacy concern where an authenticated user may log out of WordPress, only for another person to access the computer and click the back button to view the contents of the authenticated page loaded from bfcache or the HTTP cache. (See #21938.) In practice this issue depends on the user being on a shared computer who didn't exit the browser, and it requires the malicious user to act soon before the page is evicted from bfcache (e.g. Chrome as a 10-minute timeout).
To address this privacy concern, a safeguard is in place to protect against restoring pages from bfcache and the HTTP cache after the user has logged out:
When authenticating to WordPress, a “bfcache session token” cookie is set along with the other authentication cookies. This cookie is not HTTP-only so that it can be read in JavaScript; it is a random string not used for any other purpose. When an authenticated page is served, this bfcache session token is included in the HTML as well as a script which reads the value of this cookie. When a user navigates away from the page and then navigates back to it, a script on the page checks if the current session token in the cookie matches the initial session token sent with the page. If they do not match (e.g. because the user has logged out or another user has logged in), then the contents of the page are cleared and the page is reloaded so that the contents are not available.
Since JavaScript is required to invalidate cached pages, the login form is extended to pass along whether scripting is enabled. Only when JS is enabled will the no-store
directive be omitted from the Cache-Control
response header. This ensures that users with JavaScript turned off will retain the privacy protection after logging out. Lastly, no-store
is also only omitted if the user checked the “Remember Me” checkbox on the login form. Since it is highly unlikely a user on a shared computer would have checked this checkbox, this provides yet an additional safeguard (which may in the end prove excessive). A ✨ emoji is displayed next to the checkbox in a button that opens a popover that promotes the capability. If you want to opt out of this opt-in (and the sparkle) so that all logged-in users get bfcache, you can use the nocache_bfcache_use_remember_me_as_opt_in
filter which you can use in a custom plugin or your theme:
add_filter( 'nocache_bfcache_use_remember_me_as_opt_in', '__return_false' );
When this plugin strips out the no-store
directive, it also ensures that the private
directive is sent in its place: “The private
response directive indicates that the response can be stored only in a private cache (e.g., local caches in browsers).” WordPress is already sending private
as of #57627. This directive ensures that proxies do not cache authenticated pages. In addition to ensuring private
is present, this plugin also adds no-cache
, max-age=0
, and must-revalidate
while ensuring public
is removed, all to further guard against any misconfigured proxy from caching the private response.
Without bfcache:
With bfcache:
Without bfcache: The drafted BuddyPress activity update is lost when navigating away from the page before submitting. The activity feed and Tweet have to be reconstructed with each back/forward navigation.
With bfcache: The drafted BuddyPress activity update is preserved when navigating away from the page without submitting. The activity feed and Tweet do not have to be reconstructed when navigating to previously visited pages via the back/forward buttons.
- Visit Plugins > Add New in the WordPress Admin.
- Search for Instant Back/Forward.
- Install and activate the Instant Back/Forward plugin.
- Log out of WordPress and log back in with the “Remember Me” checkbox checked.
You may also install and update via Git Updater using the plugin's GitHub URL.
- Download the plugin ZIP from WordPress.org. (You may also download a development ZIP from GitHub; alternatively, if you have a local clone of the repo, run
npm run plugin-zip
.) - Visit Plugins > Add New Plugin in the WordPress Admin.
- Click Upload Plugin.
- Select the
nocache-bfcache.zip
file on your system from step 1 and click Install Now. - Click the Activate Plugin button.
- Log out of WordPress and log back in with the “Remember Me” checkbox checked.
Please see the Stale Content in Page Caches section of the above blog post.
The functionality in this plugin is proposed for WordPress core in Trac ticket #63636: Enable instant page navigations from browser history via bfcache when sending “nocache” headers.
Other relevant core tickets that this revisits:
- #21938: Add “no-store” to
Cache-Control
header to prevent history caching of admin resources - #55491: Replace
unload
event handlers from core - #57627: The Cache-Control header for logged-in pages should include
private
- #61942: Add “no-store” to
Cache-Control
header to prevent unexpected cache behavior
Instead of using the pageshow
event handler, an alternative method to evict pages from bfcache is to send the Clear-Site-Data: "cache"
at logout. Per MDN, this header “sends a signal to the client that it should remove all browsing data of certain types (cookies, storage, cache) associated with the requesting website.” This header is supposedly supported by 91%+ of users according to Can I Use… and it is "Baseline 2023 Newly available" but with an asterisk. In testing, only Chromium-based browsers (e.g. Chrome and Edge) seem to evict pages from bfcache when this header is sent, but there is currently a bug (40233601) where responses with this header can take 10-30 seconds to load. Furthermore, Firefox does not currently evict pages from bfcache with this header, but like Chromium browsers, it does evict pages from HTTP cache, meaning authenticated pages will not be accessible when reopening closed browser tabs. Safari, however, does not seem to evict pages from either bfcache or the HTTP cache. Lastly, Clear-Site-Data
only works in a secure context (i.e. over HTTPS), meaning insecure sites still on HTTP would have yet another concern.
For all these reasons, Clear-Site-Data
is not yet a reliable method to invalidate pages from bfcache. Hopefully the Chromium bug will be fixed in the near future.
The Clear-Site-Data
header was also mentioned in Trac tickets #49258 and #57627.
Chrome may even now store pages served with no-store
in bfcache, although there are still failure scenarios in which bfcache will still be blocked. These can be observed in the “Back/forward cache” panel in the Application tab of Chrome DevTools, for example:
JsNetworkRequestReceivedCacheControlNoStoreResource
: JavaScript on a page makes a request to a resource served with theno-store
directive (e.g. REST API or admin-ajax).CacheControlNoStoreCookieModified
: JavaScript on a page modifies cookies.
These scenarios happen frequently when browsing the WP Admin, and they occur frequently on the frontend when using plugins like WooCommerce or BuddyPress. Such bfcache failures can also occur when not being logged in to WordPress, as it can happen whenever a site calls nocache_headers()
. For example, WooCommerce currently calls nocache_headers()
when an unauthenticated user is on the Cart, Checkout, or My Account pages (but see woocommerce#58445 which has been merged to remove this as of v10.1). These failure scenarios do not occur when the no-store
directive is omitted from the Cache-Control
header.
See the Back/forward cache article on web.dev for reasons why bfcache may be blocked. See also the list of blocking reasons on MDN. See also the YouTube video on Debugging bfcache, make your page load instantly.
If you can identify the plugin or theme which is setting Cache-Control: no-store
or doing something else that blocks bfcache (like adding an unload
event handler), please report the issue to the respective plugin/theme support forum.
The Performance Lab plugin also includes a Site Health test for whether the server is sending the Cache-Control: no-store
header.
When Jetpack is active, you may see that bfcache isn't working on any page and that the “Back/forward cache” panel of Chrome DevTools says:
Pages with WebSocket cannot enter back/forward cache.
Here you'll also see:
Pending Support: Chrome support for these reasons is pending i.e. they will not prevent the page from being eligible for back/forward cache in a future version of Chrome.
The reason for this is the “Notifications” module of Jetpack, which shows up as a bell icon in the top right of the admin bar. If you do not rely on this feature of Jetpack, you can enable bfcache by going to WP Admin > Jetpack > Settings and in the footer click “Modules”. Here you can disable the Notifications module. Otherwise, see a filed Jetpack issue to improve the WebSocket handling so that it doesn't disable bfcache.
Aside from this, bfcache may be disabled on some Jetpack screens because the plugin is still sending no-store
. A pull request has been opened to remove these.
Lastly, the Akismet screen has an iframe
which contains a page with an unload
event listener. This event should never be used anymore; the Akismet team should replace it with a more appropriate event, as was done in core (#55491).
Pantheon sites have a must-use plugin which includes some Page Cache functionality. When a user is logged in, it is currently sending a Cache-Control: no-cache, no-store, must-revalidate
response header. This prevents bfcache from working. A pull request has been opened to fix this, but in the meantime you may work around the issue by preventing this header from being sent with the following plugin code:
// Workaround for Pantheon MU plugin sending Cache-Control: no-store which prevents bfcache.
// See https://github.com/pantheon-systems/pantheon-mu-plugin/pull/94
add_filter(
'pantheon_skip_cache_control',
static function (): bool {
return is_admin() || is_user_logged_in();
}
);
Pages are served from bfcache where previously they would fail to do so with an issue like MainResourceHasCacheControlNoStore
showing up in the “Back/forward cache” panel of the Application tab in Chrome DevTools.
- Rename plugin to "Instant Back/Forward" (#49)
- Add
nocache_bfcache_use_remember_me_as_opt_in
filter to opt out of opt-in (#29) - Ensure
is_user_logged_in()
function exists before using (#37) - Update plugin-check to v1.6 and reuse PHPCS ruleset (#27)
- Add path data for CSS file (#15)
- Only run update-dotorg-assets manually on workflow_dispatch (#16)
- Explored using
Clear-Site-Data
header for bfcache invalidation, but ultimately removed it due to browser inconsistencies (#17, #20) - Eliminate Broadcast Channel bfcache eviction method (#18)
- Integrate with frontend login forms (#19)
- Improve robustness of detecting login form submissions (#21)
- Implement HTTP cache invalidation (#23)
- Add GHA workflow (#1)
- Integrate with interim login (wp-auth-check) modal (#2)
- Log out warning when page navigation is not restored from bfcache when
WP_DEBUG
is enabled (#3) - Retain CCNS when the user has JavaScript disabled (#4)
- Improve bfcache invalidation via BroadcastChannel, store token in user session, promote feature on login (#5)
- Prepare for dotorg directory release (#6)
- Fix compatibility with the Two-Factor plugin and any plugins with interstitial login screens (#7)
- Fix bfcache invalidation via Broadcast Channel (#8)
- Improve docs with info about Jetpack and add FAQ (#9)
- Add FAQ for why bfcache may not work on Pantheon (#10)
- Add dotorg plugin directory assets (#11)
- Configure Dependabot (#12)
- Add deployment workflows (#14)
- Initial release.