The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Inspect readinessdocument.readyStateView examples
Wait for parsed DOMdocument.addEventListener('DOMContentLoaded', init, { once: true })View examples
Wait for page loadwindow.addEventListener('load', measureAssets, { once: true })View examples
Initialize late or earlydocument.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init()View examples
Read visibilitydocument.visibilityState === 'hidden'View examples
Observe visibilitydocument.addEventListener('visibilitychange', syncWork)View examples
Handle page activationwindow.addEventListener('pageshow', restoreTransientState)View examples
Handle page deactivationwindow.addEventListener('pagehide', releaseTransientResources)View examples
Detect cached restorationif (event.persisted) refreshVolatileData()View examples
Warn about unsaved workwindow.addEventListener('beforeunload', event => event.preventDefault())View examples
Avoid unload handlerswindow.addEventListener('pagehide', cleanup)View examples
Queue a small beaconnavigator.sendBeacon('/session', payload)View examples
Send with fetch optionsfetch('/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

01

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.

Initialize correctly from any loading strategy
<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>
Back to quick reference ↑
02

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.

Pause and resume polling without duplicating timers
<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>
Back to quick reference ↑
03

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.

Reconnect a transient channel after restoration
<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>
Back to quick reference ↑
04

Install beforeunload only while data is genuinely at risk

beforeunload exists for user-facing unsaved-work protection, not routine cleanup. Browsers require prior user interaction, show a generic message rather than custom text, and may not fire the event in common mobile or process-termination paths. Register it only while a dirty form needs protection and remove it after saving; avoid unload entirely because it is unreliable and can interfere with history caching.

Toggle an unsaved-changes warning
<form id="profile"><label>Display name <input name="name"></label><button>Save</button></form>
<script>
  let dirty = false;
  const warn = (event) => { event.preventDefault(); event.returnValue = true; };
  profile.addEventListener('input', () => {
    if (!dirty) window.addEventListener('beforeunload', warn);
    dirty = true;
  });
  profile.addEventListener('submit', () => {
    dirty = false;
    window.removeEventListener('beforeunload', warn);
  });
</script>
Back to quick reference ↑
05

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.

Flush a bounded activity batch when hidden
<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>
Back to quick reference ↑

Local code tester

Observe readiness and visibility transitions

See current readiness and visibility, then switch tabs or hide the preview to record visibility changes.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGHTML Standard: Document lifecyclehtml.spec.whatwg.org
  2. WHATWGHTML Standard: The Document objecthtml.spec.whatwg.org
  3. WHATWGHTML Standard: Page visibilityhtml.spec.whatwg.org
  4. World Wide Web ConsortiumBeaconw3.org
  5. WHATWGFetch Standard: keepalive requestsfetch.spec.whatwg.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback