Had an issue with bfcache
By Flavio Copes
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: