Skip to content

Back/Forward Cache (bfcache)

The browser back/forward cache keeps a full snapshot of a page in memory when the visitor navigates away, so pressing Back or Forward restores it instantly instead of reloading it. Venta 1.7.2 and newer supports it: the theme resets its own component state when a page is restored. Turning it on takes two steps - enable the admin setting, and make the reverse proxy change. Without the proxy change bfcache stays off.

The admin setting

Stores > Configuration > Hyva Themes > System > Cache Options > Enable Bfcache (the same field also appears under Advanced > System > Full Page Cache).

When enabled, Hyvä's RemoveNoStoreHeaderPlugin strips the no-store directive from the Cache-Control header of responses Magento marked as cacheable. Responses for cart, checkout and customer account pages keep no-store and stay out of bfcache.

The setting is off by default. Enable it per environment in the admin, or from the command line:

bash
bin/magento config:set system/full_page_cache/bfcache 1

Required: the reverse proxy must stop sending no-store

A page with Cache-Control: no-store is never placed in bfcache by Chrome or Firefox. The Varnish configuration Venta projects start from (.docker/varnish/default.vcl in the reference setup) rewrites the Cache-Control header of every non-static response on the way out, and that rewrite includes no-store. Magento can strip the directive all it wants - Varnish puts it back.

In vcl_deliver, replace the blanket rewrite with one that keeps no-store only for responses Magento refused to cache:

vcl
# Not letting browser to cache non-static files.
if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(pub/)?(media|static)/") {
    set resp.http.Pragma = "no-cache";
    set resp.http.Expires = "-1";
    if (obj.uncacheable) {
        set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
    } else {
        set resp.http.Cache-Control = "no-cache, must-revalidate, max-age=0";
    }
}

Do not delete no-store unconditionally

The Hyvä documentation suggests removing no-store from the rewrite line outright. On this VCL that is unsafe: Magento sends no-store, no-cache, must-revalidate, max-age=0 without a private directive for uncacheable pages (cart, checkout, customer account), so they match the same condition and the blunt edit makes them bfcache-eligible too. The obj.uncacheable guard keeps them excluded.

The change takes effect only after Varnish reloads the VCL: restart the varnish container in the Docker setup, or run varnishreload on a VM.

The same applies to any other edge layer. If the project runs Fastly or another CDN that sets no-store on HTML responses, the equivalent change is needed there.

The Magento core patch is not an alternative here

The Hyvä documentation also mentions a core patch (magento/magento2#40750) that covers both the PHP and Varnish sides. On Venta it does not help: its Varnish part patches the VCL templates Magento generates, not the hand-maintained VCL Venta projects deploy, and its JavaScript part resets Luma components, not Hyvä/Alpine ones. The PHP side is already covered by Hyvä's plugin with the admin setting. Use the VCL change above instead.

Two consequences:

  • The admin toggle does not gate the Varnish rewrite. Turning the setting off restores no-store only on responses that reach the browser without the rewrite. If you need bfcache fully off behind Varnish, revert the VCL change as well.
  • Verify with headers, not with the toggle. curl -sI https://<store>/ | grep -i cache-control must not contain no-store on a cacheable page, and must contain it on /checkout/cart.

What the theme does on a restore

A restored page resumes with its JavaScript state exactly as it was captured, so anything that was open stays open and any cached customer data stays stale unless something resets it. Venta handles this in two layers.

A global guard in the theme bundle (web/tailwind/js/bundle/bfcache.js) runs on every restore:

  • It dispatches reload-customer-section-data, which refetches customer section data and updates the header, cart counter and login state. This is what prevents the classic failure: log out, press Back, and the restored page still greets the logged-in customer.
  • It reloads the page outright when the restored page is a private one (body class starting with customer-, checkout-, sales- or wishlist-). Safari places these in bfcache even with no-store, so a JavaScript guard is the only control that holds in every browser.

On top of that, each component that holds open or overlay state closes itself on restore: the cart drawer, customer menu, authentication popup, messages, mobile menu, search overlay and the layered navigation filter modal. The last three also lock body scroll through hyva.bodyLock(); the lock is part of the snapshot, so without the reset a restored page could not be scrolled.

Themes with their own bundle entry must import the guard

bfcache.js is a new theme bundle file in 1.7.2. If your theme keeps its own venta-bundle.js with a hardcoded import list (instead of using the theme package's entry), add the import when updating:

js
import '.../vendor/magebitcom/magento2-venta-theme/web/tailwind/js/bundle/bfcache.js';

Without it the built bundle silently omits the guard while the per-component resets keep working, since their files were already on the import list. The gap then only shows as stale customer data on restored pages. The file stays a theme bundle file on purpose: a project that needs different guard behaviour can point this import at its own copy instead.

Custom components need a pageshow handler

Any custom Alpine component that keeps open/visible state needs the same treatment. Listen for pageshow and reset when event.persisted is true:

js
function initMyOverlay() {
    return {
        open: false,
        close() {
            window.hyva?.bodyUnlock();
            this.open = false;
        },
        closeOnPageShow(event) {
            if (event.persisted && this.open) {
                this.close();
            }
        }
    };
}
window.addEventListener('alpine:init', () => {
    Alpine.data('initMyOverlay', initMyOverlay);
}, { once: true });
html
<div x-data="initMyOverlay" @pageshow.window="closeOnPageShow">

Two rules to hold on to:

  • x-spread does nothing on Alpine 3. It was the Alpine 2 way to bind a handler object and Alpine 3 ignores the attribute silently, with no console error. Use x-bind, and check for leftover x-spread in components copied from older themes.
  • Components that lock body scroll must reset through a method that unlocks it. Setting the open flag directly leaves the locked class on body and the restored page cannot be scrolled.

Verifying

Test in a real Chrome or Firefox window

Embedded browsers (Electron shells, IDE previews, in-app browsers) disable bfcache and report a masked blocking reason no matter what the page does. Results from them are meaningless for this feature.

Chrome DevTools has a purpose-built tester under Application > Back/forward cache > Test back/forward cache. It navigates away and back, then either confirms the restore or lists the blocking reasons. Cache-Control: no-store in that list means the proxy change has not taken effect for this response.

The same check is scriptable from the console after pressing Back (Chromium only; in Firefox and Safari listen for pageshow and read event.persisted instead):

js
performance.getEntriesByType('navigation')[0].notRestoredReasons
// null means the page was served from bfcache

Then walk the stale-state scenarios:

  1. Log in, add items to the cart, log out, press Back. The restored page must show guest state and no cart count.
  2. Change the cart from another tab, press Back in the first. The count must match the server.
  3. Open each overlay (menu, search, filters, cart drawer), navigate away, press Back. Everything closed, page scrolls.
  4. Add a product to the cart from a restored page. This confirms the restored page's form key is still valid: the add must succeed.

Limitations

  • Only pages Magento caches benefit. Any page with a form key (contact, login, checkout, customer account) is uncacheable by design and stays out of bfcache in Chrome and Firefox. Intended behaviour, not a defect.
  • Modals restore open. Venta's native dialog modals are not reset on restore. This is harmless: they stay closable, and closing them releases any scroll lock. It also matches upstream Hyvä behaviour, so Venta leaves it as is.
  • A rebuilt JS bundle needs a hard refresh while testing. venta-bundle.min.js is served with a one-year cache lifetime. When testing changes to bfcache.js or component reset code, a normal reload keeps running the old bundle: hard refresh, or keep "Disable cache" active in DevTools.