DEV Community

Cover image for Your Back button is losing you sales: fixing bfcache on e-commerce sites
not the same
not the same

Posted on

Your Back button is losing you sales: fixing bfcache on e-commerce sites

A shopper opens a product, taps Back, and the catalog loads from scratch: filters reset, scroll position lost, infinite scroll back to page one. On mobile, where people jump between the list and product pages a dozen times per visit, this is one of the quietest conversion killers we see in e-commerce audits.

Browsers already have a fix for it. It's called the back/forward cache (bfcache): when the user navigates away, the browser keeps a snapshot of the whole page in memory, JS heap included, and restores it instantly on Back. No network, no re-render, same scroll, same filters.

The catch: a lot of stores quietly opt out of it without knowing. Here's how to find out why and fix it.

1. Check if your page is eligible

Chrome DevTools has a built-in test:

DevTools → Application → Back/forward cache → Test back/forward cache

It navigates away and back, then lists the exact reasons the page wasn't restored. Run it on your catalog, product page and search results. Those are the pages where Back is used the most.

2. Get the reasons from real users

DevTools tells you about your machine. For field data, Chrome exposes notRestoredReasons on the navigation entry:

const [nav] = performance.getEntriesByType('navigation');

if (nav.type === 'back_forward' && nav.notRestoredReasons) {
  // send it to your analytics to see blockers across real sessions
  console.log(nav.notRestoredReasons);
}
Enter fullscreen mode Exit fullscreen mode

A back/forward navigation that shows up as a regular page load means bfcache was missed. Logging the reasons for a week usually shows one or two scripts responsible for most of the misses.

3. The usual blockers

unload listeners

The classic one. An unload handler makes the page ineligible in most browsers, and it's often not even your code: old analytics snippets, chat widgets, A/B testing tools.

// ❌ blocks bfcache
window.addEventListener('unload', sendStats);

// ✅ fires on navigation and keeps the page eligible
window.addEventListener('pagehide', sendStats);

// ✅ even better for analytics: also covers tab switching and app close on mobile
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    navigator.sendBeacon('/collect', JSON.stringify(payload));
  }
});
Enter fullscreen mode Exit fullscreen mode

To find who registers unload, search your bundle and third-party tags, or check the DevTools test above. It names the frame.

Cache-Control: no-store on public pages

no-store is one of the most common reasons pages are excluded from bfcache. It makes sense on the cart, checkout and account pages. It doesn't make sense on a catalog page that is the same for every visitor, but frameworks and caching plugins sometimes send it site-wide, for example once a session cookie is set.

Check the response headers of your category pages:

curl -sI https://your-store.com/category/shoes/ | grep -i cache-control
Enter fullscreen mode Exit fullscreen mode

If public pages come back with no-store, find where it's set (middleware, plugin, CDN rule) and limit it to pages with personal data.

Open connections

Open WebSockets, WebRTC connections and some other live connections can prevent caching. Close them when the page is hidden and reopen them when it comes back:

let socket;

function connect() {
  socket = new WebSocket('wss://example.com/live');
}

window.addEventListener('pagehide', () => socket?.close());

window.addEventListener('pageshow', (event) => {
  if (event.persisted) connect();
});

connect();
Enter fullscreen mode Exit fullscreen mode

4. Handle the restore properly

Once bfcache works, remember the page comes back exactly as it was left, including stale data. On a store that usually means the cart badge:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // restored from bfcache: refresh data that may have changed
    refreshCartCount();
    refreshStockBadges();
  }
});
Enter fullscreen mode Exit fullscreen mode

The same applies to analytics: a bfcache restore doesn't fire a new page load, so if you want those page views counted, send them from pageshow with event.persisted === true.

5. Don't break it with infinite scroll

bfcache restores the in-memory page, so infinite scroll works fine when the cache hits. When it misses, the user lands at the top of page one. A cheap fallback is to keep the state in the URL:

// after loading page 3 with filters applied
const url = new URL(location.href);
url.searchParams.set('page', '3');
history.replaceState(null, '', url);
Enter fullscreen mode Exit fullscreen mode

Then a cache miss still brings the shopper back to the same listing instead of the start.

Quick checklist

  • Run the DevTools bfcache test on catalog, product and search pages.
  • Replace unload with pagehide or visibilitychange, including in third-party tags.
  • Keep Cache-Control: no-store only on pages with personal data.
  • Close WebSockets on pagehide, reopen on pageshow.
  • Refresh the cart and stock on pageshow when event.persisted is true.
  • Keep filters and pagination in the URL as a fallback.

Back navigation is something every shopper does many times per visit, and it's usually free to fix. If you're working on mobile performance more broadly, we covered LCP, INP and CLS in our PageSpeed optimization guide, and other places where stores lose buyers in this CRO checklist.

We're not the same, a web development studio from Ukraine working on e-commerce and B2B projects.

Top comments (0)