Skip to content

How to Handle Page Lifecycle Events in JavaScript

How to use JavaScript page lifecycle events like DOMContentLoaded, load, beforeunload, and unload to initialize UI, handle unsaved changes, and optimize performance.

Every web page goes through a lifecycle: the browser starts downloading HTML, parses it into a DOM tree, loads external resources like images and stylesheets, and eventually the page is fully ready for the user. When the user navigates away, the page goes through a teardown sequence. At each stage of this lifecycle, the browser fires specific events that let your JavaScript code respond at exactly the right moment.

Understanding these lifecycle events is essential for writing reliable web applications. Running code too early means DOM elements might not exist yet. Running code too late means the user sees an unfinished page. Knowing the difference between DOMContentLoaded and load determines whether your interactive features initialize in milliseconds or seconds. And handling beforeunload correctly can prevent users from accidentally losing unsaved work.

This guide covers every page lifecycle event in order: from the initial HTML parsing (DOMContentLoaded) through full resource loading (load), to page departure (beforeunload and unload). You will also learn about document.readyState and the readystatechange event, which let you check and react to the page's current loading stage at any point.

DOMContentLoaded: HTML Parsed, DOM Ready

The DOMContentLoaded event fires on the document object when the browser has finished parsing the HTML and building the complete DOM tree. At this point, all HTML elements exist as DOM nodes and can be queried and manipulated with JavaScript.

This is the earliest point at which you can safely interact with the DOM.

Basic Usage

document.addEventListener("DOMContentLoaded", () => {
  console.log("DOM is ready!");

  // Safe to access any element in the HTML
  let header = document.getElementById("main-header");
  let buttons = document.querySelectorAll(".action-btn");
  let form = document.querySelector("form");

  console.log(`Found ${buttons.length} buttons`);
});

What Is and Is Not Ready

When DOMContentLoaded fires:

Ready:

  • The complete DOM tree (all HTML elements are nodes you can access)
  • Inline <style> elements have been parsed
  • Scripts that appeared before the event have executed

Not necessarily ready:

  • Images may still be loading (<img> elements exist but may have width/height of 0)
  • External stylesheets may still be loading
  • Iframes may still be loading their content
  • Fonts may still be downloading
  • Other external resources (<video>, <audio>) may still be loading
document.addEventListener("DOMContentLoaded", () => {
  let img = document.querySelector("img");

  // The element EXISTS in the DOM
  console.log(img); // <img src="photo.jpg">

  // But it may not be fully loaded yet
  console.log(img.naturalWidth);    // May be 0 (image still downloading)
  console.log(img.complete);        // May be false

  // DOM operations work perfectly
  let container = document.getElementById("app");
  container.classList.add("initialized");
});

DOMContentLoaded and Scripts

Regular <script> tags (without async or defer) block HTML parsing. The browser must download and execute the script before it continues parsing the rest of the HTML. This means DOMContentLoaded waits for all synchronous scripts to finish:

<!DOCTYPE html>
<html>
<head>
  <title>Page</title>
</head>
<body>
  <h1>Hello</h1>

  <script>
    // This script runs BEFORE DOMContentLoaded
    // Parsing pauses here until this script finishes
    console.log("Script executed");
  </script>

  <p>World</p>

  <script>
    document.addEventListener("DOMContentLoaded", () => {
      // This runs AFTER all HTML is parsed and all synchronous scripts have run
      console.log("DOMContentLoaded fired");
    });
  </script>
</body>
</html>

Output order:

Script executed
DOMContentLoaded fired

DOMContentLoaded and Stylesheets

Stylesheets themselves do not block DOMContentLoaded. However, if a <script> tag appears after a <link rel="stylesheet">, the browser must wait for the stylesheet to load before executing the script (because the script might read computed styles). This indirectly delays DOMContentLoaded:

<head>
  <!-- This stylesheet loads... -->
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <p>Content</p>

  <!-- This script must wait for styles.css to load first -->
  <!-- (because it might read computed styles) -->
  <script>
    // This script is blocked until styles.css is loaded
    // DOMContentLoaded waits for this script
    console.log("Script after stylesheet");
  </script>
</body>

If there is no <script> after the stylesheet, the stylesheet does not block DOMContentLoaded.

DOMContentLoaded and defer / async Scripts

Scripts with the defer attribute download in parallel with HTML parsing but execute after parsing is complete and before DOMContentLoaded:

<head>
  <script defer src="app.js"></script>
</head>
<body>
  <h1>Hello</h1>
  <script>
    document.addEventListener("DOMContentLoaded", () => {
      console.log("DOMContentLoaded");
    });
  </script>
</body>

Execution order:

(HTML parsing completes)
(app.js executes - defer scripts run here)
DOMContentLoaded

Scripts with the async attribute download in parallel and execute as soon as they are downloaded, without waiting for HTML parsing. They do not block DOMContentLoaded (unless they happen to finish downloading before parsing is done):

<head>
  <script async src="analytics.js"></script>
</head>
<body>
  <!-- async scripts do NOT reliably run before DOMContentLoaded -->
  <!-- They might run before or after, depending on download speed -->
</body>

When to Use DOMContentLoaded

Use DOMContentLoaded when your code needs to access DOM elements but does not depend on images, fonts, or other external resources being fully loaded:

document.addEventListener("DOMContentLoaded", () => {
  // Initialize UI interactions
  setupNavigation();
  setupFormValidation();
  setupEventDelegation();

  // Manipulate the DOM
  document.getElementById("year").textContent = new Date().getFullYear();

  // Start fetching data
  loadUserProfile();
});

What If DOMContentLoaded Already Fired?

If you add a DOMContentLoaded listener after the event has already fired, your callback will never run:

// If this code runs after DOMContentLoaded has already fired:
document.addEventListener("DOMContentLoaded", () => {
  console.log("This will never execute!");
});

To handle this case safely, check document.readyState (covered later):

function onDOMReady(callback) {
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", callback);
  } else {
    // DOM is already ready
    callback();
  }
}

onDOMReady(() => {
  console.log("This always works, regardless of timing!");
});

load: All Resources Loaded

The load event fires on the window object when everything on the page has finished loading: the DOM, all images, stylesheets, iframes, fonts, and other external resources. This is the absolute last loading event.

Basic Usage

window.addEventListener("load", () => {
  console.log("Everything is fully loaded!");

  // Now images have their real dimensions
  let img = document.querySelector("img");
  console.log(`Image size: ${img.naturalWidth} x ${img.naturalHeight}`);

  // All fonts are loaded, text is rendered with final fonts
  // All iframes have loaded their content
  // All stylesheets are applied
});

DOMContentLoaded vs. load: Timing Difference

The time difference between these two events can be significant, especially on pages with large images or many external resources:

let startTime = performance.now();

document.addEventListener("DOMContentLoaded", () => {
  let domTime = performance.now() - startTime;
  console.log(`DOM ready: ${domTime.toFixed(0)}ms`);
});

window.addEventListener("load", () => {
  let loadTime = performance.now() - startTime;
  console.log(`Full load: ${loadTime.toFixed(0)}ms`);
});

// Typical output:
// DOM ready: 150ms
// Full load: 2400ms
// (The load event fires much later because images take time to download)

When to Use load

Use load only when your code genuinely needs all resources to be available. This is less common than DOMContentLoaded:

window.addEventListener("load", () => {
  // Calculate layout based on actual image dimensions
  let images = document.querySelectorAll(".gallery img");
  images.forEach(img => {
    let aspectRatio = img.naturalWidth / img.naturalHeight;
    img.parentElement.style.aspectRatio = aspectRatio;
  });

  // Initialize a canvas that depends on loaded images
  let canvas = document.getElementById("preview");
  let ctx = canvas.getContext("2d");
  let heroImage = document.getElementById("hero-img");
  ctx.drawImage(heroImage, 0, 0); // Only works if image is fully loaded

  // Hide the loading spinner
  document.getElementById("loading-spinner").hidden = true;

  // Measure performance
  let perfEntries = performance.getEntriesByType("navigation");
  if (perfEntries.length > 0) {
    console.log(`Page load time: ${perfEntries[0].loadEventEnd}ms`);
  }
});

load on Individual Elements

The load event also fires on individual resource elements when they finish loading:

// Image load
let img = new Image();
img.addEventListener("load", () => {
  console.log("Image loaded!");
  document.body.append(img);
});
img.addEventListener("error", () => {
  console.log("Image failed to load!");
});
img.src = "/photos/landscape.jpg";

// Script load
let script = document.createElement("script");
script.addEventListener("load", () => {
  console.log("Script loaded and executed!");
});
script.addEventListener("error", () => {
  console.log("Script failed to load!");
});
script.src = "https://cdn.example.com/library.js";
document.head.append(script);

beforeunload: Preventing Accidental Leave

The beforeunload event fires on window when the user is about to leave the page. This includes clicking a link to another page, typing a new URL, closing the tab, closing the browser, or refreshing the page.

Its primary purpose is to warn users about unsaved changes before they navigate away.

Basic Usage

window.addEventListener("beforeunload", (event) => {
  // Cancel the event to trigger the browser's confirmation dialog
  event.preventDefault();
});

When this event is handled, the browser shows a built-in confirmation dialog asking the user if they really want to leave. The dialog appearance and text are controlled entirely by the browser. You cannot customize the message in modern browsers.

Only Show When There Are Unsaved Changes

You should only trigger the beforeunload dialog when the user actually has unsaved work. Showing it unconditionally is annoying and considered bad UX:

let hasUnsavedChanges = false;

// Track changes
let form = document.getElementById("editor-form");
form.addEventListener("input", () => {
  hasUnsavedChanges = true;
});

// Clear the flag when the form is saved
form.addEventListener("submit", () => {
  hasUnsavedChanges = false;
});

// Only warn when there are unsaved changes
window.addEventListener("beforeunload", (event) => {
  if (hasUnsavedChanges) {
    event.preventDefault();
  }
  // If hasUnsavedChanges is false, the page navigates away normally
});

A More Complete Pattern

class UnsavedChangesGuard {
  constructor() {
    this.isDirty = false;
    this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
    window.addEventListener("beforeunload", this.handleBeforeUnload);
  }

  markDirty() {
    this.isDirty = true;
  }

  markClean() {
    this.isDirty = false;
  }

  handleBeforeUnload(event) {
    if (this.isDirty) {
      event.preventDefault();
    }
  }

  destroy() {
    window.removeEventListener("beforeunload", this.handleBeforeUnload);
  }
}

// Usage
let guard = new UnsavedChangesGuard();

// When user edits content
document.getElementById("editor").addEventListener("input", () => {
  guard.markDirty();
});

// When content is saved
async function saveContent() {
  await fetch("/api/save", { method: "POST", body: getContent() });
  guard.markClean();
}

Browser Behavior and Restrictions

Modern browsers have strict restrictions on beforeunload to prevent abuse:

Custom messages are ignored. In the past, you could set event.returnValue to a custom string that would appear in the dialog. Modern browsers ignore custom messages and show their own generic text (like "Changes you made may not be saved"):

window.addEventListener("beforeunload", (event) => {
  event.preventDefault();

  // ❌ Custom messages are IGNORED in modern browsers
  event.returnValue = "You have unsaved changes!"; // Ignored
  return "Are you sure?"; // Also ignored
  // The browser shows its own generic message instead
});

The dialog requires recent user interaction. Some browsers (notably Chrome) may not show the dialog if the user has not interacted with the page at all (no clicks, no keystrokes). This prevents pages from trapping users who opened a link and immediately want to close it.

unload: Page Is Being Left

The unload event fires on window when the user is actually leaving the page. At this point, the navigation is committed and cannot be stopped. The page is in the process of being torn down.

Basic Usage

window.addEventListener("unload", () => {
  console.log("Page is being unloaded");
});

Extremely Limited Usefulness

The unload event is severely limited in what you can do inside it:

  • You cannot prevent navigation (that is beforeunload's job).
  • You cannot use alert(), confirm(), or prompt() (they are blocked).
  • Most asynchronous operations will be canceled (the page is being destroyed).
  • Regular fetch or XMLHttpRequest calls may be aborted before completing.

Sending Data on Page Exit with navigator.sendBeacon

The one practical use for unload (or beforeunload) is sending analytics or cleanup data to a server. Regular fetch calls may be canceled during page unload, but navigator.sendBeacon() is designed specifically for this purpose. It guarantees the request is queued for delivery even after the page is destroyed:

window.addEventListener("unload", () => {
  // sendBeacon sends data reliably during page unload
  let data = JSON.stringify({
    event: "page_exit",
    timeOnPage: Math.round(performance.now() / 1000),
    scrollDepth: Math.round(
      (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
    )
  });

  navigator.sendBeacon("/api/analytics", data);
});

navigator.sendBeacon:

  • Sends a POST request asynchronously.
  • Is guaranteed to be queued even during page unload.
  • Does not delay the navigation (it is non-blocking).
  • Accepts a Blob, FormData, URLSearchParams, or string as the body.
// Sending with different data formats
navigator.sendBeacon("/api/log", "simple string data");

navigator.sendBeacon("/api/log", new URLSearchParams({
  event: "exit",
  page: location.pathname
}));

let blob = new Blob([JSON.stringify({ action: "leave" })], {
  type: "application/json"
});
navigator.sendBeacon("/api/log", blob);

pagehide: The Modern Alternative to unload

Modern browsers recommend using pagehide instead of unload. The pagehide event works similarly but is compatible with the back-forward cache (bfcache), which lets browsers instantly restore pages when the user clicks the back button:

window.addEventListener("pagehide", (event) => {
  if (event.persisted) {
    // Page is being cached in bfcache (user might come back)
    console.log("Page cached for potential restore");
  } else {
    // Page is actually being unloaded
    console.log("Page is being destroyed");
  }

  // sendBeacon works here too
  navigator.sendBeacon("/api/analytics", JSON.stringify({
    event: "page_hide",
    cached: event.persisted
  }));
});

document.readyState

The document.readyState property tells you the current loading state of the document at any point in time. It is a string with one of three values, representing three stages of the page lifecycle.

The Three States

console.log(document.readyState);
// One of: "loading", "interactive", "complete"
State Meaning Corresponds To
"loading" The HTML document is still being parsed. The DOM is not yet fully built. Before DOMContentLoaded
"interactive" The HTML has been fully parsed and the DOM tree is complete. External resources (images, stylesheets, iframes) may still be loading. DOMContentLoaded is about to fire (or has just fired)
"complete" The document and all external resources have finished loading. load event is about to fire (or has just fired)

The Lifecycle Timeline

Here is the complete sequence of events and state changes during page load:

[1] document.readyState = "loading"
        ↓
    (HTML parsing in progress...)
        ↓
[2] document.readyState = "interactive"
        ↓
[3] DOMContentLoaded event fires
        ↓
    (External resources loading: images, stylesheets, iframes...)
        ↓
[4] document.readyState = "complete"
        ↓
[5] window "load" event fires

You can verify this order:

// Place this in a <script> inside <head>
console.log("1. Initial readyState:", document.readyState); // "loading"

document.addEventListener("readystatechange", () => {
  console.log("readystatechange →", document.readyState);
});

document.addEventListener("DOMContentLoaded", () => {
  console.log("DOMContentLoaded, readyState:", document.readyState);
});

window.addEventListener("load", () => {
  console.log("load, readyState:", document.readyState);
});

Output:

1. Initial readyState: loading
readystatechange → interactive
DOMContentLoaded, readyState: interactive
readystatechange → complete
load, readyState: complete

Using readyState for Safe Initialization

The most important use of readyState is handling the case where your code might run after DOMContentLoaded has already fired (for example, a dynamically loaded script):

function onDOMReady(fn) {
  if (document.readyState === "loading") {
    // DOM not yet ready - wait for it
    document.addEventListener("DOMContentLoaded", fn);
  } else {
    // DOM is already ready ("interactive" or "complete")
    fn();
  }
}

// This works regardless of when the script loads
onDOMReady(() => {
  console.log("Safe to access the DOM!");
  initializeApp();
});

A more complete version that also handles waiting for full page load:

function onPageReady(fn) {
  if (document.readyState === "complete") {
    fn();
  } else {
    window.addEventListener("load", fn);
  }
}

function onDOMReady(fn) {
  if (document.readyState !== "loading") {
    fn();
  } else {
    document.addEventListener("DOMContentLoaded", fn);
  }
}

Checking readyState at Any Time

You can check readyState at any point to make decisions about what is safe to do:

function initGallery() {
  if (document.readyState === "complete") {
    // All images are loaded: can read natural dimensions
    setupGalleryWithDimensions();
  } else if (document.readyState === "interactive" || document.readyState === "complete") {
    // DOM is ready: can access elements, but images may not be loaded
    setupGalleryBasic();
    // Wait for images separately
    window.addEventListener("load", () => {
      updateGalleryDimensions();
    });
  } else {
    // Still loading HTML: wait for DOM
    document.addEventListener("DOMContentLoaded", () => {
      setupGalleryBasic();
    });
    window.addEventListener("load", () => {
      updateGalleryDimensions();
    });
  }
}

readystatechange Event

The readystatechange event fires on document every time document.readyState changes. It fires exactly twice during a normal page load: once when readyState changes from "loading" to "interactive", and once when it changes from "interactive" to "complete".

Basic Usage

document.addEventListener("readystatechange", () => {
  console.log(`readyState changed to: ${document.readyState}`);
});

// Output during page load:
// readyState changed to: interactive
// readyState changed to: complete

Using readystatechange as an Alternative to DOMContentLoaded and load

You can use readystatechange to react to both lifecycle stages with a single listener:

document.addEventListener("readystatechange", () => {
  switch (document.readyState) {
    case "interactive":
      // Equivalent to DOMContentLoaded
      console.log("DOM is ready");
      initializeUI();
      break;

    case "complete":
      // Equivalent to window load
      console.log("All resources loaded");
      startAnimations();
      hideLoadingScreen();
      break;
  }
});

Precise Event Order

Here is the exact order of all lifecycle events with readystatechange included:

let log = [];

document.addEventListener("readystatechange", () => {
  log.push(`readystatechange: ${document.readyState}`);
});

document.addEventListener("DOMContentLoaded", () => {
  log.push("DOMContentLoaded");
});

window.addEventListener("load", () => {
  log.push("window load");
  console.log(log.join("\n"));
});

Output:

readystatechange: interactive
DOMContentLoaded
readystatechange: complete
window load

The precise order is:

  1. readystatechange (to "interactive")
  2. DOMContentLoaded
  3. readystatechange (to "complete")
  4. window load

Practical Example: Page Lifecycle Monitor

Here is a complete example that visualizes the entire page lifecycle in real time, demonstrating all the events and states covered in this guide:

<!DOCTYPE html>
<html>
<head>
  <title>Page Lifecycle Demo</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
    .event-log { list-style: none; padding: 0; }
    .event-log li {
      padding: 10px 16px;
      margin: 6px 0;
      border-radius: 6px;
      font-family: monospace;
      font-size: 14px;
      display: flex;
      justify-content: space-between;
    }
    .event-log .time { color: #888; }
    .state-loading { background: #fff3e0; border-left: 4px solid #ff9800; }
    .state-interactive { background: #e3f2fd; border-left: 4px solid #2196f3; }
    .state-domready { background: #e8f5e9; border-left: 4px solid #4caf50; }
    .state-complete { background: #f3e5f5; border-left: 4px solid #9c27b0; }
    .state-load { background: #fce4ec; border-left: 4px solid #e91e63; }
    .state-info { background: #f5f5f5; border-left: 4px solid #9e9e9e; }

    .status-bar {
      padding: 12px 20px;
      border-radius: 8px;
      font-weight: bold;
      font-size: 16px;
      text-align: center;
      margin-bottom: 20px;
      transition: background-color 0.3s;
    }
    .unsaved-demo { margin-top: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
    .unsaved-demo textarea { width: 100%; height: 80px; margin: 10px 0; padding: 8px; font-size: 14px; }
    .unsaved-indicator { font-size: 14px; padding: 4px 10px; border-radius: 4px; display: inline-block; }
    .unsaved-indicator.clean { background: #e8f5e9; color: #2e7d32; }
    .unsaved-indicator.dirty { background: #fce4ec; color: #c62828; }
  </style>

  <script>
    // Start timing from the very beginning
    var startTime = performance.now();
    var events = [];

    function logEvent(name, cssClass) {
      var elapsed = (performance.now() - startTime).toFixed(1);
      events.push({ name, cssClass, time: elapsed });
    }

    // Log initial state
    logEvent("readyState: " + document.readyState, "state-loading");

    // Track readystatechange
    document.addEventListener("readystatechange", function() {
      var cls = document.readyState === "interactive" ? "state-interactive" : "state-complete";
      logEvent("readystatechange → " + document.readyState, cls);
    });

    // Track DOMContentLoaded
    document.addEventListener("DOMContentLoaded", function() {
      logEvent("DOMContentLoaded", "state-domready");
      renderLog();
    });

    // Track load
    window.addEventListener("load", function() {
      logEvent("window load", "state-load");
      logEvent("readyState: " + document.readyState, "state-info");
      renderLog();
      updateStatus("complete");
    });
  </script>
</head>
<body>
  <h1>Page Lifecycle Events</h1>
  <div class="status-bar" id="status" style="background: #fff3e0;">
    Loading...
  </div>
  <ul class="event-log" id="event-log"></ul>

  <!-- Images to delay the load event -->
  <p>Images below delay the <code>load</code> event:</p>
  <img src="https://picsum.photos/400/200?random=1" alt="Random image 1" width="400" height="200"
       onload="logEvent('Image 1 loaded', 'state-info'); renderLog();">
  <img src="https://picsum.photos/400/200?random=2" alt="Random image 2" width="400" height="200"
       onload="logEvent('Image 2 loaded', 'state-info'); renderLog();">

  <div class="unsaved-demo">
    <h3>Unsaved Changes Demo</h3>
    <p>Type in the textarea below, then try to close or refresh the tab.</p>
    <textarea id="editor" placeholder="Type something..."></textarea>
    <div>
      <span class="unsaved-indicator clean" id="save-status">No changes</span>
      <button id="save-btn" style="margin-left: 10px;">Save</button>
    </div>
  </div>

  <script>
    function renderLog() {
      var list = document.getElementById("event-log");
      if (!list) return;
      list.innerHTML = "";
      events.forEach(function(e) {
        var li = document.createElement("li");
        li.className = e.cssClass;
        li.innerHTML = '<span>' + e.name + '</span><span class="time">' + e.time + ' ms</span>';
        list.appendChild(li);
      });
    }

    function updateStatus(state) {
      var status = document.getElementById("status");
      if (state === "complete") {
        status.style.background = "#e8f5e9";
        status.textContent = "Page fully loaded (readyState: complete)";
      }
    }

    // Unsaved changes guard
    var isDirty = false;
    var editor = document.getElementById("editor");
    var saveStatus = document.getElementById("save-status");
    var saveBtn = document.getElementById("save-btn");

    editor.addEventListener("input", function() {
      isDirty = true;
      saveStatus.textContent = "Unsaved changes!";
      saveStatus.className = "unsaved-indicator dirty";
    });

    saveBtn.addEventListener("click", function() {
      isDirty = false;
      saveStatus.textContent = "Saved";
      saveStatus.className = "unsaved-indicator clean";
    });

    window.addEventListener("beforeunload", function(event) {
      if (isDirty) {
        event.preventDefault();
      }
    });

    // Log beforeunload and pagehide
    window.addEventListener("beforeunload", function() {
      navigator.sendBeacon("/api/log", JSON.stringify({
        event: "beforeunload",
        timeOnPage: Math.round(performance.now() / 1000)
      }));
    });

    window.addEventListener("pagehide", function(event) {
      console.log("pagehide, persisted:", event.persisted);
    });
  </script>
</body>
</html>

This example demonstrates:

  • Lifecycle event order: Each event is logged with a timestamp, showing the exact order and timing
  • readystatechange transitions from loading to interactive to complete
  • DOMContentLoaded firing after readyState becomes interactive
  • Image load events firing between DOMContentLoaded and window load
  • window load firing last, after all images are loaded
  • beforeunload with unsaved changes detection
  • navigator.sendBeacon for reliable data sending during page exit

Summary

The page lifecycle consists of a predictable sequence of events that let your code run at exactly the right moment.

Loading Events (in order):

Event/State Fires On When Use For
readyState: "loading" document Initial state while HTML is being parsed Checking if DOM is ready yet
readyState: "interactive" document HTML fully parsed, DOM tree complete (Use DOMContentLoaded instead)
DOMContentLoaded document DOM is ready, external resources may still be loading Initializing UI, attaching events, DOM manipulation
readyState: "complete" document All resources loaded (Use load instead)
load window Everything loaded: images, styles, iframes, fonts Working with image dimensions, hiding loading screens

Unloading Events:

Event Fires On When Use For
beforeunload window User is about to leave Warning about unsaved changes (call event.preventDefault())
pagehide window Page is being hidden or unloaded Analytics, cleanup (bfcache-friendly alternative to unload)
unload window Page is being destroyed Last-chance data sending with sendBeacon (avoid if possible)

document.readyState:

  • "loading": HTML is being parsed
  • "interactive": DOM is ready (just before DOMContentLoaded)
  • "complete": All resources loaded (just before load)
  • Use it to safely initialize code that might run after events have already fired

Key Guidelines:

  • Use DOMContentLoaded for most initialization (faster than load).
  • Use load only when you need fully loaded resources (images, fonts).
  • Use beforeunload sparingly and only for genuine unsaved changes.
  • Use navigator.sendBeacon() for reliable data sending during page exit.
  • Prefer pagehide over unload to preserve back-forward cache compatibility.
  • Use document.readyState checks to handle late-loading scripts safely.
Navigation