101. How would you restore back-forward cache eligibility without breaking page state?
A commerce application reloads whenever users press Back, and DevTools reports that pages are excluded from the back-forward cache because of an unload handler and an open cross-page communication resource. Describe how you would confirm every blocker, replace incompatible lifecycle logic, pause and resume timers or connections around pagehide and pageshow, and verify restored state after a persisted navigation. Include tests that distinguish a bfcache restore from a normal reload.
I would first reproduce the Back navigation and use the browser Back forward cache diagnostics to list every exclusion reason. I would remove the unload handler, remove any unnecessary beforeunload handler, and move lifecycle cleanup to pagehide. I would pause timers, animation work, observers, and cross page connections there, then resume only the resources that need to run when pageshow fires. I would use event.persisted to detect a real cached restore, avoid repeating one time initialization, and verify the result with DevTools, the navigation entry type, the Network panel, and preserved page state.
See the Code while reading this explanation.
When a shopper presses Back, the browser should be able to show the previous page immediately instead of loading the document again. Here, some page behavior stops the browser from keeping that page ready in memory. I would first find every blocking behavior. Then I would replace unsafe leaving page logic with browser lifecycle events that work with cached restoration. I would pause work while the page is stored, restart it when the page returns, and check that the cart, form values, scroll position, media state when relevant, and other visible state are still correct.
- Which browsers and commerce routes show the Back navigation reload?
- What cross page resource stays open, such as a WebSocket or BroadcastChannel?
- Which state must remain exactly as the shopper left it?
I would start with the user visible symptom: pressing Back reloads the document instead of restoring the previous page from the Back forward cache. My baseline is a repeatable navigation from the affected commerce page to another page and then Back, using the same browser, device class, production build, cache state, and steps each time. The success metric is a persisted restore with no new document request and with the page state still correct.
First, I would open the browser Back forward cache diagnostics and reproduce the navigation. I would record every reported exclusion reason instead of stopping after the first one. In this case, the known blockers are an unload handler and an open cross page communication resource. I would also review any other reasons that DevTools reports, such as an unnecessary beforeunload handler, a pending IndexedDB transaction, synchronous request work, or another resource that cannot remain active across the navigation.
Next, I would remove the unload listener. If a beforeunload listener is not required to protect unsaved user work, I would remove it too. Cleanup moves to pagehide. When pagehide runs, I stop polling timers, cancel pending animation work, disconnect observers when appropriate, and close WebSocket or BroadcastChannel resources that should not remain active while the page is stored. If analytics must be sent while leaving, I would use navigator.sendBeacon or another lifecycle safe request instead of relying on unload.
The pagehide event has a persisted flag. When it is true, the browser is preserving the page for a cached history restore. The DOM, JavaScript heap, scroll position, and normal in memory application state stay with that frozen page. I would not rebuild that state unnecessarily. I would save only volatile state that the application also needs as a fallback after a normal reload.
When pageshow fires, event.persisted being true tells me that this specific page display came from the Back forward cache. I would reconnect the socket or channel, restart polling, reconnect observers, and schedule visual work again. The restart functions must be idempotent so repeated Back and Forward navigation does not create duplicate timers, listeners, subscriptions, or connections. I would also skip one time initialization that must run only on a normal load.
For verification, I would repeat the same navigation after the change. DevTools should report that the page is eligible and show a restored Back forward cache navigation. In pageshow I would confirm event.persisted is true. I would also inspect performance.getEntriesByType("navigation")[0].type. A value of "back_forward" supports that this was a history navigation, but it is not enough by itself to prove a cached restore. The Network panel should show no new document request for the restored page, although a deliberately reopened socket or other resumed connection can create expected network activity.
Finally, I would verify correctness. The cart, form inputs, scroll position, focus behavior, media position when relevant, and other application state should match what the shopper left behind. I would confirm that only one timer, observer, socket, or channel is active after each restore. I would also test the normal reload path because pagehide and pageshow must work correctly when event.persisted is false. The final result is a page that is eligible for the Back forward cache, restores quickly on Back, and preserves correct application behavior.
- Reproduce the Back navigation on the affected commerce route with the same browser, device class, build, cache state, and steps.
- Use the browser Back forward cache diagnostics to record every exclusion reason.
- Remove the unload handler and remove an unnecessary beforeunload handler.
- Replace leaving page cleanup with pagehide.
- On pagehide, pause polling and timers, cancel pending animation work, disconnect observers, and close cross page connections that cannot remain active.
- Let the browser preserve normal DOM and JavaScript memory state. Save only state that is also needed for a normal reload fallback.
- On pageshow, use event.persisted to tell a cached restore from a normal page display.
- Resume resources with idempotent restart functions and skip one time initialization on a cached restore.
- Repeat the same navigation and confirm event.persisted is true, the navigation type is back_forward, DevTools reports a restore, and no new document request appears.
- Verify cart state, form values, scroll position, focus behavior, media state when relevant, and that no timer, observer, socket, channel, listener, or subscription is duplicated.
const appState = {
cart: { items: [] },
};
let pollingId = null;
let animationFrameId = null;
let socket = null;
let didInitialSetup = false;
// Observe only while the page is active.
const observer = new ResizeObserver(() => {
// Application specific resize work belongs here.
});
function startPolling() {
// Start only one polling timer after a normal load or cached restore.
if (pollingId !== null) return;
pollingId = window.setInterval(() => {
// Application specific polling work belongs here.
}, 5000);
}
function stopPolling() {
// Stop the active timer before the page is frozen or discarded.
if (pollingId === null) return;
window.clearInterval(pollingId);
pollingId = null;
}
function scheduleAnimation() {
// Schedule visual work only while the page is active.
if (animationFrameId !== null) return;
animationFrameId = window.requestAnimationFrame(() => {
animationFrameId = null;
// Application specific visual update belongs here.
});
}
function cancelAnimation() {
// Cancel pending visual work before leaving the active state.
if (animationFrameId === null) return;
window.cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
function openSocket() {
// Avoid duplicate connections after repeated Back and Forward restores.
if (
socket &&
(socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)
) {
return;
}
socket = new WebSocket('wss://example.com/cart');
}
function closeSocket() {
// Close the live connection before the page is stored or discarded.
if (!socket) return;
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
socket.close(1000, 'pagehide');
}
socket = null;
}
function saveFallbackState() {
// Save only state that is also useful after a normal reload.
sessionStorage.setItem('cart', JSON.stringify(appState.cart));
}
function restoreFallbackState() {
// Read optional fallback state without rebuilding all cached page state.
const saved = sessionStorage.getItem('cart');
if (saved) {
appState.cart = JSON.parse(saved);
}
}
function flushLeaveAnalytics() {
// sendBeacon is suitable for small fire and forget leave analytics.
const payload = JSON.stringify({ event: 'pagehide' });
navigator.sendBeacon('/analytics', payload);
}
function connectActiveResources() {
// Resume only resources that should run while this page is active.
startPolling();
openSocket();
observer.observe(document.documentElement);
scheduleAnimation();
}
function disconnectActiveResources() {
// Pause resources before the browser freezes or discards this page.
stopPolling();
cancelAnimation();
observer.disconnect();
closeSocket();
}
function runInitialSetupOnce() {
// Keep one time initialization separate from cached restore work.
if (didInitialSetup) return;
didInitialSetup = true;
// Register one time application behavior here.
}
window.addEventListener('pagehide', (event) => {
// pagehide replaces unload for lifecycle cleanup.
disconnectActiveResources();
saveFallbackState();
flushLeaveAnalytics();
// This is useful diagnostic information while testing eligibility.
console.log('pagehide persisted:', event.persisted);
});
window.addEventListener('pageshow', (event) => {
// A true persisted flag is the direct signal for a cached restore.
if (event.persisted) {
restoreFallbackState();
connectActiveResources();
console.log('Back forward cache restore:', true);
return;
}
// A normal page display runs normal startup once.
runInitialSetupOnce();
connectActiveResources();
});
function reportNavigationType() {
// The navigation entry distinguishes history navigation from reload.
const entry = performance.getEntriesByType('navigation')[0];
const type = entry ? entry.type : 'unknown';
console.log('Navigation type:', type);
console.log('History navigation:', type === 'back_forward');
}
reportNavigationType();Interviewers ask this to see whether I understand browser page lifecycle behavior, can use browser diagnostics to find every Back forward cache blocker, and can change cleanup logic without losing page state. They also want to see whether I can separate a real cached restore from a normal history reload and verify that timers, connections, user interface state, and application data still behave correctly.
A common mistake is fixing only the first blocker that DevTools reports and not checking again for additional exclusion reasons. Another is keeping unload or an unnecessary beforeunload handler even after moving some cleanup elsewhere. Developers can also reconnect timers, observers, sockets, channels, listeners, or subscriptions on every pageshow without guarding against duplicates. Another mistake is rebuilding the whole application even though a cached page already keeps its DOM and JavaScript memory. It is also wrong to treat the back_forward navigation type alone as proof of a cached restore. A pending IndexedDB transaction or synchronous request can remain a blocker, so those must be removed or completed before navigation rather than ignored.
Explain this as a browser lifecycle problem. Start with the Back navigation reload, show how DevTools identifies every blocker, move cleanup from unload to pagehide, resume only paused resources on pageshow, and finish by proving both the cached restore and correct page state.










