The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Inspect readiness | document.readyState | View examples |
| Wait for parsed DOM | document.addEventListener('DOMContentLoaded', init, { once: true }) | View examples |
| Wait for page load | window.addEventListener('load', measureAssets, { once: true }) | View examples |
| Initialize late or early | document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init() | View examples |
| Read visibility | document.visibilityState === 'hidden' | View examples |
| Observe visibility | document.addEventListener('visibilitychange', syncWork) | View examples |
| Handle page activation | window.addEventListener('pageshow', restoreTransientState) | View examples |
| Handle page deactivation | window.addEventListener('pagehide', releaseTransientResources) | View examples |
| Detect cached restoration | if (event.persisted) refreshVolatileData() | View examples |
| Warn about unsaved work | window.addEventListener('beforeunload', event => event.preventDefault()) | View examples |
| Avoid unload handlers | window.addEventListener('pagehide', cleanup) | View examples |
| Queue a small beacon | navigator.sendBeacon('/session', payload) | View examples |
| Send with fetch options | fetch('/session', { method: 'POST', body: payload, keepalive: true }) | View examples |
A page does not simply load once and unload once. Scripts can run during parsing, documents become hidden without navigating, and session-history traversal can freeze and later restore the same page from the back-forward cache. Robust applications initialize idempotently, derive work from current visibility, release transient resources on pagehide, restore them on pageshow, and never depend on unload-time blocking work.
Step by step
Detailed examples
Initialize against readiness, not script-placement assumptions
document.readyState is loading while parsing, interactive after parsing completes, and complete after load-delaying resources finish. DOMContentLoaded is appropriate for DOM-dependent initialization; load is for work that truly needs completed subresources. A module loaded dynamically may execute after DOMContentLoaded, so reusable initialization code should check readyState and be safe to call once.
<output id="status">Waiting</output>
<script>
let initialized = false;
function init() {
if (initialized) return;
initialized = true;
status.value = `Ready: ${document.readyState}`;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
</script> Derive expensive work from the current visibility state
visibilityState describes whether the page is visible or hidden; it is not the same as window focus. Use visibilitychange to pause animation, polling, telemetry batching, or media that should not continue in the background, then resume from current application state. Hidden documents can be throttled or later discarded, so do not treat repeated timers as a durable clock.
<output id="activity"></output>
<script>
let timer;
function syncWork() {
clearInterval(timer);
timer = undefined;
if (document.visibilityState === 'visible') {
activity.value = 'Polling';
timer = setInterval(() => console.log('refresh'), 30000);
} else {
activity.value = 'Paused';
}
}
document.addEventListener('visibilitychange', syncWork);
syncWork();
</script> Design for back-forward cache suspension and restoration
A page placed in the back-forward cache can retain its heap and DOM, then reappear without a new script evaluation or DOMContentLoaded event. pagehide and pageshow bracket page deactivation and activation. PageTransitionEvent.persisted indicates a preserved history entry in these events. Release short-lived resources such as locks or open channels on pagehide and reacquire or refresh volatile data on pageshow.
<output id="connection">Disconnected</output>
<script>
let channel;
function connect() {
channel?.close();
channel = new BroadcastChannel('inventory');
connection.value = 'Connected';
}
window.addEventListener('pageshow', (event) => {
connect();
if (event.persisted) console.log('Restored from session history');
});
window.addEventListener('pagehide', () => {
channel?.close();
channel = undefined;
connection.value = 'Disconnected';
});
</script> Send final small payloads without blocking departure
When the document becomes hidden, sendBeacon can queue a small POST and returns whether it was queued; it offers no response callback. fetch with keepalive supports more request configuration and a response promise, but the request may outlive the page and both mechanisms have user-agent payload limits. Save important data continuously and server-side; lifecycle delivery is best-effort, not a transaction guarantee.
<script>
const events = [];
document.addEventListener('click', (event) => {
events.push({ tag: event.target.localName, at: Date.now() });
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'hidden' || events.length === 0) return;
const payload = new Blob([JSON.stringify(events.splice(0))], { type: 'application/json' });
if (!navigator.sendBeacon('/activity', payload)) console.warn('Beacon was not queued');
});
</script> Local code tester
Observe readiness and visibility transitions
See current readiness and visibility, then switch tabs or hide the preview to record visibility changes.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



