How to Debug Browser Tests That Fail After Back-Forward Cache Restores a Page With Stale DOM State or Detached Event Handlers
By David Frei · September 23, 2026
Learn how to tell when a browser test used BFCache, which assertions become unreliable after back and forward navigation, and how to separate app bugs from cache behavior.
A test that passes on reload and fails after Back or Forward is often not broken in the obvious way. The page may have been restored from the browser’s back-forward cache (BFCache), which means the document was put back almost as-is instead of being reloaded from the network. That changes what your test sees, especially if it assumes a fresh DOM, a new JavaScript runtime, or reattached event listeners.
The quickest way to reason about these failures is to ask one question first: did the browser restore the page from BFCache, or did it actually reload it? If you do not answer that early, you can spend hours debugging “stale DOM state” that is really a navigation model mismatch.
BFCache is not a bug by itself. It is a browser optimization that becomes test-relevant when your assertions depend on page lifecycle, object identity, or side effects from a full reload.
What BFCache changes for browser automation
BFCache stores a page snapshot so the browser can return to it quickly on back/forward navigation. The important detail for test writers is that the restored page often keeps its DOM tree, JavaScript heap, and event listeners alive in a frozen state, then resumes later. That is very different from a cold navigation.
For automation, that means:
window.onloadmay not run again.- your app’s boot code may not rerun,
- event listeners may still exist, but references captured in closures may be stale,
- DOM nodes you cached before navigation may no longer match what your locator or framework sees after restore,
- tests that wait for “page loaded” can become misleading if the page was restored without a network request.
MDN’s BFCache documentation and web.dev’s guidance both emphasize that the page is restored from memory-like state, not reloaded from scratch. For browser test authors, that distinction drives the entire debugging process.
First, distinguish the failure type
There are three different problems that look similar in a flaky report:
- App state bug
- Your app incorrectly preserves state across navigation.
- Example: a modal remains open after back navigation when it should close.
- Automation assumption bug
- Your test assumes a reload and waits for the wrong event.
- Example: waiting for a network idle state after Back when there is no network navigation.
- Framework handle bug
- Your code holds stale references to elements or page objects.
- Example: a Selenium
WebElementcaptured before navigation is used after restore.
A BFCache restore can expose all three, but the fix is different in each case.
A decision tree for separating app bugs from BFCache behavior
Use this sequence when a test fails after back navigation.
1. Did the browser actually perform a reload?
Check whether the page was restored from BFCache by logging navigation timing and page lifecycle events.
If you can instrument the app or the test page, record:
pageshowevent andevent.persistedpagehideevent andevent.persistedperformance.getEntriesByType('navigation')[0].type
In a BFCache restore, pageshow fires again and event.persisted is often true. The navigation timing entry may show a back-forward style navigation rather than a fresh load.
2. Did the page state survive as designed?
If BFCache was used, ask whether the observed state is valid across restore.
- If state should survive, your test should assert that it survives.
- If state should reset, the app likely needs explicit lifecycle handling for
pageshoworpagehide.
3. Are you using stale element references or cached page objects?
If your framework reuses DOM handles across navigation, re-query the element after restore.
4. Does the failure reproduce when BFCache is disabled?
If the test passes when BFCache is off, the failure is about restore behavior, not ordinary navigation.
5. Does it reproduce in multiple browsers?
BFCache eligibility and lifecycle behavior differ across browsers and versions. A test that is stable in one browser may still fail in another because the page is eligible for BFCache in one and not the other.
What to log before changing the test
A reproducible BFCache issue needs more than a screenshot. Capture evidence that tells you whether the page was restored and what changed across navigation.
Logging checklist
Record these items in your test run output:
- browser name and version,
- browser channel or driver version,
- OS,
- test framework version,
- navigation sequence, for example
home -> product -> back, - whether the back navigation used a user gesture or browser history API,
pageshow.persistedandpagehide.persisted,- current URL before and after navigation,
- a snapshot of visible text or key DOM text before and after restore,
- whether any network request happened during the back navigation,
- whether the failure only occurs on a specific browser.
If possible, also log whether your page registers unload handlers, because unload-related behavior can influence BFCache eligibility.
Minimal browser-side instrumentation
This small snippet helps confirm whether a restore happened and whether the page lifecycle was replayed the way you expected.
<script>
window.addEventListener('pageshow', (event) => {
console.log('pageshow', {
persisted: event.persisted,
url: location.href,
navType: performance.getEntriesByType('navigation')[0]?.type
});
});
window.addEventListener(‘pagehide’, (event) => { console.log(‘pagehide’, { persisted: event.persisted, url: location.href }); }); </script>
This does not fix the bug, but it narrows the cause quickly.
Assertions that become unreliable after BFCache restore
Some assertions are valid only after a true reload. These are the first ones I would review.
1. Assertions tied to load or initial boot code
If the test waits for an event or marker that fires only on a cold page load, it may pass when reloaded and fail when restored. A BFCache restore may skip the initialization path you were waiting for.
2. Assertions based on one-time side effects
Examples include:
- analytics beacons sent during startup,
- API calls from boot code,
- initialization of global variables,
- session-scoped state cleared during full page load.
If your test expects those side effects after Back, you may be asserting the wrong lifecycle.
3. Cached element handles
Framework handles can go stale in different ways:
- Selenium
WebElementreferences can become invalid after DOM changes or navigation. - Playwright locators are safer because they re-resolve, but you can still make stale assumptions if you keep custom state outside the locator.
If the test stores a node reference and later clicks it after navigation, re-query it instead.
4. Assumptions about event handler reattachment
A detached event handler is often not literally detached in BFCache terms. More often, the handler is still attached to an old DOM tree or an old closure environment, and your test is interacting with an object graph that is not the one your app logic expects.
If the app re-renders after restore, any listener wiring that depends on a mount path may need to run on pageshow, not only on DOMContentLoaded.
5. Assertions that rely on URL change alone
The URL may change during history navigation even when the DOM is restored from BFCache. URL assertions alone do not prove a fresh load.
If a test only checks the URL, it can miss the exact failure mode that BFCache introduced, namely that the page looks new at the address bar but is old in memory.
Reproducing the failure consistently
To debug browser tests fail after back forward cache in a controlled way, you want a repeatable navigation sequence and a page that exposes lifecycle events.
Playwright example
This example shows how to capture the navigation sequence and inspect the lifecycle signals after going back.
import { test, expect } from '@playwright/test';
test('back navigation lifecycle', async ({ page }) => {
const logs: string[] = [];
page.on('console', msg => logs.push(msg.text()));
await page.goto('https://example.com/page-a');
await page.click('a[href="/page-b"]');
await page.goBack();
await expect(page.locator('h1')).toBeVisible();
console.log(logs);
});
If the page under test logs pageshow.persisted=true, you know the browser restored state rather than rebuilding the page.
Selenium Python example
With Selenium, the main goal is to avoid carrying stale element references across history navigation.
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome() driver.get(‘https://example.com/page-a’) driver.find_element(By.CSS_SELECTOR, ‘a[href=”/page-b”]’).click() driver.back()
heading = driver.find_element(By.TAG_NAME, ‘h1’) print(heading.text)
Notice the element is re-fetched after back(). That small change removes one source of false positives.
Browser-side causes that can block BFCache
Sometimes your test is flaky because BFCache is sometimes used and sometimes not. That inconsistency often comes from page features that affect eligibility.
Common areas to inspect include:
unloadhandlers,- certain active network behaviors,
- pages that keep resources open in a way the browser cannot cache safely,
- conditions that vary across browsers or versions.
You do not need to memorize every eligibility rule to debug a test. You only need to know whether your page is eligible in the path you are testing, because eligibility affects whether the page is restored or reloaded.
How to fix the app, not just the test
Once you confirm the behavior is really BFCache restore state, decide whether the application should adapt.
If state should survive
Use restore-aware logic.
- Rehydrate UI from existing state on
pageshow. - Make code resilient when initialization runs again or does not run at all.
- Avoid assuming
DOMContentLoadedis the only point where setup can occur.
If state should reset
Make the reset explicit.
- Clear UI state on restore if the previous page state is no longer valid.
- Revalidate session data after
pageshow. - Re-query the backend if the page data might have changed while the page was frozen.
If the test should not depend on BFCache at all
Change the test to make its assumption explicit.
- Use a fresh page context if the scenario is about cold startup.
- Re-open the page directly instead of relying on browser history.
- Assert on app-visible state, not on an implementation detail like
loadfiring.
Practical debugging checklist
When a test fails after back navigation, work through this sequence:
- Confirm whether the browser restored the page from BFCache.
- Log
pageshow.persisted,pagehide.persisted, and navigation type. - Re-fetch any element handles after history navigation.
- Check whether your wait conditions assume a full reload.
- Compare behavior across browsers and browser versions.
- Disable BFCache only as a diagnostic, not as a permanent workaround.
- Decide whether the app or the test should change based on the expected UX.
When the failure is probably not BFCache
Do not blame BFCache automatically. The issue is more likely something else if:
- the page fails on direct reload, not just back navigation,
- the same stale element error appears with no history navigation,
- the app has a real routing bug unrelated to browser restore behavior,
- the failure happens only after a data mutation, not after navigation,
- the DOM changes because the app re-renders incorrectly after a state update.
A compact rule of thumb
If a browser test fail after back forward cache symptoms appear, the fastest path is this:
- Reload path fails, debug the app or the test setup.
- Back path fails, reload path passes, inspect BFCache restore behavior and stale references.
- Back path only fails in one browser, compare eligibility and lifecycle behavior in that browser first.
FAQ
How do I know if a page came from BFCache?
Log pageshow and check event.persisted. Pair that with performance.getEntriesByType('navigation')[0]?.type and compare it with a normal reload.
Why do my event handlers stop working after back navigation?
They may not have stopped. The page may have been restored with an old DOM tree or an old closure state, so the handler is still attached but operating on stale assumptions.
Should I disable BFCache to make tests stable?
Only as a diagnostic step. Disabling it permanently can hide real lifecycle bugs and make your tests less representative of user behavior.
Which assertions are safest after back navigation?
Assertions that inspect current, re-queried UI state are safer than assertions that depend on initial load hooks, cached element handles, or one-time bootstrap side effects.
Is BFCache a browser bug?
No. It is a browser feature. The bug is usually either an app lifecycle assumption that does not survive restore, or a test that assumes every navigation is a reload.
Primary references
- MDN, Back-forward cache
- web.dev, Back/forward cache
- Playwright documentation, Navigation
- Selenium documentation, Navigation