Had an issue with bfcache

By

Learn how the browser bfcache served a stale htmx counter in Chrome on back and forward, and how pageshow with event.persisted helps you refresh client state.

~~~

Today I learned about bfcache when someone pointed out a problem on an example I did of a counter in htmx.

You load the page, counter (whose state is stored on the server) is at 0.

Increment counter, 1, 2, 3. Increment happens without a full page reload.

Go back to a previous page in your browser with the back button, then go forward again to the counter.

Counter is at 0 again.

Not because it reset, but because the browser cached the first full page load in its bfcache (back/forward cache).

You need to refresh the page to see the actual count value.

This only happened in Chrome (and Chrome-based browsers like Arc). Safari? No problem. Firefox? No problem. And also, Chrome with DevTools open with “Cache disabled” setting? No problem (that’s why I didn’t notice).

What I tried first

I added these meta tags to my HTML head tag:

<meta http-equiv="Cache-Control" content="no-cache, no-store" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />

I could also set them as HTTP headers, for example using Astro:

import { defineConfig } from 'astro/config'
import node from '@astrojs/node'

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone',
  }),
  server: {
    headers: {
      'Cache-Control': 'no-cache, no-store',
      Pragma: 'no-cache',
      Expires: '0',
    },
  },
})

Only a few headers work as meta http-equiv, and Cache-Control is not one of them. Browsers ignore it there. Set the real response header instead.

I had a hard time testing this because I couldn’t find a good way to clear the bfcache, until I had the idea of testing in incognito mode, and I was able to see it worked fine.

What changed since then

Read more about bfcache: https://web.dev/articles/bfcache

Cache-Control: no-store used to keep Chrome out of bfcache. Around March–April 2025, Chrome rolled bfcache for safe no-store pages out to everyone. So no-store is no longer a reliable way to opt out in Chrome. See Chrome’s bfcache + Cache-Control: no-store notes.

The better pattern for pages with stale client state (like my counter) is to refresh that state when the page comes back from bfcache:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // restored from bfcache, refresh the stale client state here
    console.log('This page was restored from the bfcache.')
  } else {
    console.log('This page was loaded normally.')
  }
})

event.persisted is true only on a real bfcache restore. If it stays false while you test, the page never entered bfcache. Open DevTools, go to Application, then Back/forward cache, and run the test. The panel tells you why the page was not cached.

If you still need a hard reload as a last resort:

window.addEventListener(
  'pageshow',
  function (event) {
    if (
      event.persisted ||
      performance.getEntriesByType('navigation')[0].type === 'back_forward'
    ) {
      location.reload()
    }
  },
  false,
)

…not optimal, of course.

But interesting.

Never heard of bfcache before.

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about html: