How to Fix bfcache Eligibility Blockers
This guide is the hands-on workflow for Back/Forward Cache (bfcache), part of Advanced Caching Strategies & CDN Architecture. The scenario: your field data shows that most back navigations reload the page instead of restoring it, and DevTools lists a handful of reasons when you test. Each reason corresponds to an API or header that the browser cannot safely freeze and resume. Most have a standard replacement that keeps the feature and restores eligibility.
Work in order of field frequency rather than DevTools order. A blocker that affects 60% of misses is worth fixing first even if it looks minor; a blocker that appears only when a rare third-party widget loads can wait.
Rapid Diagnosis
- Run DevTools → Application → Back/forward cache → Test. Note each reason and its category: "Actionable", "Pending support" or "Not actionable".
- Collect field reasons.
performance.getEntriesByType('navigation')[0].notRestoredReasonsonpageshowwhenpersistedis false and the type isback_forward. - Search the codebase for
addEventListener('unload',onunload,beforeunload,new WebSocket(,RTCPeerConnection, and IndexedDB transactions held across awaits. - List third-party scripts and iframes on the template; many blockers live there.
Root Cause Analysis: The Blockers and Why They Block
1. unload handlers. Code in unload assumes the page is being destroyed. If the browser froze the page instead, that assumption would break, so Chromium treats pages with unload handlers as ineligible on desktop (and Firefox similarly).
2. Cache-Control: no-store on the document. The header signals the content must not be stored; browsers have historically refused to keep such pages in bfcache.
3. Open network connections. WebSockets, WebRTC peer connections and in-flight fetches cannot be paused indefinitely.
4. Locks and transactions. Unfinished IndexedDB transactions, Web Locks or other shared resources held by the page would block other tabs while frozen.
5. Blocked frames. Any of the above in an iframe — including cross-origin ads and embeds — blocks the whole page.
Step-by-Step Resolution
1. Replace unload with pagehide or visibilitychange
// Before
addEventListener('unload', () => navigator.sendBeacon('/log', JSON.stringify(pending)));
// After: fires on bfcache entry AND on real unloads; does not block bfcache.
addEventListener('pagehide', () => navigator.sendBeacon('/log', JSON.stringify(pending)));
// trade-off: pagehide also fires when the page enters bfcache, so code that
// "cleans up for good" must be able to undo itself on pageshow if restored.
Expected outcome: the most common blocker disappears. Details in replacing unload handlers for bfcache.
2. Close connections on pagehide, reopen on pageshow
let socket = connect();
addEventListener('pagehide', (e) => { if (e.persisted) socket.close(1000, 'bfcache'); });
addEventListener('pageshow', (e) => { if (e.persisted) socket = connect(); });
// trade-off: messages sent while the page was frozen are missed. Fetch a delta
// ("what changed since timestamp X") when reconnecting.
Expected outcome: pages with live data become eligible without losing updates.
3. Keep IndexedDB transactions short
Never hold a transaction open across awaits on network requests or user input; complete writes promptly so no transaction is pending when the user navigates away.
4. Narrow no-store to pages that need it
Use Cache-Control: no-cache, private for pages that must be revalidated but are not secret; reserve no-store for genuinely sensitive content. See Cache-Control no-store and bfcache.
5. Deal with blocking iframes
Update vendors, lazy-load embeds so they are not present on pages users commonly navigate back to, or replace them with facades that create the iframe only on interaction.
Verification
Re-run the DevTools test for each template until it reports successful restoration. In the field, track the restoration rate and the distribution of remaining reasons weekly; it should climb as each fix ships and the remaining reasons should be ones outside your control. Add an end-to-end check for key templates so regressions are caught before release.
Worked Example: Removing Three Blockers from a News Article
A news article template had a 9% restoration rate. DevTools reported an unload handler (from a scroll-depth analytics script), an open WebSocket (live comment count) and a cross-origin iframe (a social embed). The analytics vendor offered a configuration flag to use visibilitychange; the comment count socket was closed on pagehide and the count refreshed on restore; social embeds were replaced with a facade that loaded the iframe on click. Restoration rate rose to 81%, and back navigations from articles to section pages became instant for most users.
Common Mistakes
- Replacing
unloadwithbeforeunload.beforeunloadis also problematic in some browsers and should only be added conditionally (for unsaved changes), then removed. - Fixing only the DevTools reason. Field data often shows different, user-specific blockers (logged-in widgets, consent-dependent vendors).
- Closing connections without reopening them. Restored pages then show stale live data indefinitely.
- Assuming a fix in one template applies site-wide. Each template has its own set of scripts and embeds.
Edge Cases
Unload handlers added conditionally. Some libraries add unload only in certain states (an unsaved form, an open modal). Test the states users are in when they navigate away, not just the default.
Extensions. Browser extensions can inject blockers. Field data will include some reasons you cannot reproduce; filter by frequency.
Back navigation to a POST result. Pages loaded via form POST have their own caching rules; prefer the POST-redirect-GET pattern so the page users return to is a cacheable GET.
Mobile vs desktop. Chromium on Android has historically been more lenient with unload than desktop; measure both platforms separately.
Prioritising Fixes Across a Large Site
On a site with dozens of templates, blockers are not evenly distributed. Build a simple matrix: templates as rows (ordered by back/forward navigation volume, not total traffic), reasons as columns, and miss counts in the cells. Back/forward volume matters because templates users return to — listings, search results, feeds, category pages — benefit far more than terminal pages like checkout confirmation. Fix the cells with the largest counts on the highest-volume rows first. Shared blockers (a site-wide analytics unload handler, a global no-store rule) usually dominate and are fixed once for every template; template-specific ones (a live widget on one page type) come next. Re-run the matrix after each release so the next fix is always chosen by data rather than by which blocker is easiest to understand.
FAQ
Which reasons are worth ignoring?
Those marked "Not actionable" in DevTools (browser limitations) and rare field reasons affecting a tiny share of misses. Focus on reasons that cover most misses on your highest-traffic templates.
Does an empty unload handler still block bfcache?
Yes. The presence of the listener is what matters, not what it does. Remove it entirely.
Can a Content Security Policy or other header block bfcache?
Most headers do not; Cache-Control: no-store is the notable exception. Some features enabled by headers, such as certain cross-origin isolation setups with specific APIs in use, can have their own restrictions, which DevTools will list.
How do I fix a blocker inside a third-party iframe?
You cannot change its code, so change how you use it: update to a newer vendor version, load it only after interaction, load it on fewer pages, or replace it. Report the issue to the vendor — many have fixed bfcache blockers after customer requests.
Will fixing blockers change my analytics numbers?
It can. Restored pages fire pageshow but not a new page load, so analytics that only count loads will under-count views. Most analytics libraries now handle pageshow with persisted; verify yours does.
Related
- Measuring bfcache hit rate in RUM — tracking the effect of each fix.
- Restoring state on pageshow after bfcache — the other half of reconnecting.
- Replacing YouTube embeds with a facade — a facade pattern that also helps bfcache.