
The Resurrection Pattern: Why Your App Needs the Page Lifecycle API to Survive Silent Tab Discarding
The browser is quietly killing your background tabs to save memory, and without a lifecycle-aware state strategy, your users are losing their work without warning.
I had spent twenty minutes meticulously filling out a complex cloud infrastructure configuration form. I hopped over to Slack to answer a quick ping, and when I clicked back to the browser ten minutes later, the tab flickered, reloaded, and presented me with a cold, empty form. It was as if my work had never existed—a victim of the browser's silent memory executioner.
The Memory Hungry Beast
Modern browsers are in a constant tug-of-war with your RAM. To keep your machine from sounding like a jet engine, Chrome, Edge, and Safari have become incredibly aggressive about "discarding" tabs.
When you navigate away from a tab, the browser might first freeze it (halting CPU usage) and eventually discard it (wiping it from memory entirely). To the user, the tab is still there in the strip at the top. But the moment they click it, the browser has to perform a full reload. If your app isn't prepared for this "resurrection," any unsaved state—form inputs, scroll positions, or unsaved drafts—vanishes into the ether.
Enter the Page Lifecycle API
The Page Lifecycle API gives us the hooks we need to handle these transitions gracefully. Instead of just hoping the user hits "Save," we can treat the browser's intervention as a standard part of our app's lifecycle.
The states you actually need to care about are:
1. Hidden: The user switched tabs or minimized the window.
2. Frozen: The browser suspended execution to save CPU.
3. Discarded: The browser killed the process to save RAM.
Here is how you catch these transitions in the wild:
const getState = () => {
return {
inputValue: document.querySelector('#my-input').value,
timestamp: Date.now()
};
};
// Handle transitions to 'hidden' or 'frozen'
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// The user moved away. This is your best chance to save state.
persistState(getState());
}
});
window.addEventListener('freeze', () => {
// The browser is about to freeze the tab.
// Last-second cleanup happens here.
persistState(getState());
});The Resurrection Pattern
To survive a discard, we need to implement what I call the Resurrection Pattern. It’s a three-step dance:
1. Persist: Save the UI state to sessionStorage or IndexedDB as soon as the tab becomes hidden.
2. Detect: On page load, check if the browser previously discarded this tab.
3. Restore: If it was discarded, pull that state back from storage and rebuild the UI.
Step 1: Saving the State
Don't wait for a "submit" button. Users don't submit until they are done, but the browser might kill your tab while they are looking up a reference in another window.
function persistState(state) {
// We use sessionStorage because it survives reloads but
// gets cleared when the tab is actually closed.
sessionStorage.setItem('app_state_resurrection', JSON.stringify(state));
}Step 2: Detecting the Discard
How do you know if a page load is a "fresh start" or a "resurrection"? Chrome provides a specific property for this: document.wasDiscarded.
window.addEventListener('DOMContentLoaded', () => {
if (document.wasDiscarded) {
// Welcome back from the dead!
const savedState = sessionStorage.getItem('app_state_resurrection');
if (savedState) {
restoreUI(JSON.parse(savedState));
}
}
});
function restoreUI(state) {
const input = document.querySelector('#my-input');
if (input) {
input.value = state.inputValue;
console.log(`Restored state from: ${new Date(state.timestamp).toLocaleTimeString()}`);
}
}Why not just use beforeunload?
You’ll see a lot of old tutorials suggesting beforeunload. Don't rely on it.
Modern browsers are moving away from beforeunload because it’s a reliability nightmare—it often doesn't fire on mobile, and it can interfere with the browser's "Back/Forward Cache" (bfcache), which makes navigations instant. visibilitychange is the gold standard now. It’s much more reliable and fires consistently across desktop and mobile.
Dealing with the "Zombie" Edge Cases
There’s a catch. If you restore state blindly, you might end up showing the user incredibly stale data. Imagine someone leaves a tab open for three days, the browser discards it, and then they click back. Do they want those three-day-old half-finished form fields?
I usually include a "stale check":
function restoreUI(state) {
const THREE_HOURS = 3 * 60 * 60 * 1000;
const isStale = (Date.now() - state.timestamp) > THREE_HOURS;
if (isStale) {
console.warn("Discarded state found, but it's too old. Starting fresh.");
sessionStorage.removeItem('app_state_resurrection');
return;
}
// Proceed with restoration...
}Summary
The web isn't a persistent environment. It's a series of ephemeral moments that the browser tries its best to stitch together. By using the Page Lifecycle API, you stop treating tab discarding as a "crash" and start treating it as a manageable state transition.
Your users won't notice when you implement the Resurrection Pattern. And that’s exactly the point—they’ll just notice that for some reason, your app never seems to lose their work. In a world of flaky tabs and memory-hungry browsers, that's a superpower.


