How to Handle Page Lifecycle Events in JavaScript
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 havewidth/heightof 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();
});
If your <script> tag is at the end of <body> (just before </body>), all HTML elements above it are already parsed. In that case, you may not need DOMContentLoaded at all, since your script can access everything above it. However, wrapping code in DOMContentLoaded is a good defensive practice, especially when scripts might be moved or when using bundlers that place scripts in <head>.
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`);
}
});
In most cases, prefer DOMContentLoaded over load. Waiting for load delays your JavaScript initialization unnecessarily if you do not need images or other external resources. Users see a faster, more responsive page when interactive features initialize at DOMContentLoaded.
If you need to know when a specific image loads, use the load event on that individual image element instead of waiting for the entire page:
let img = document.querySelector(".hero-image");
img.addEventListener("load", () => {
console.log("Hero image loaded:", img.naturalWidth, "x", img.naturalHeight);
});
// Handle case where image is already cached
if (img.complete) {
console.log("Image was already loaded (cached)");
}
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.
Use beforeunload only for genuinely important unsaved data. Never use it to:
- Keep users on your page (dark pattern)
- Show advertisements before leaving
- Track page exits (use
navigator.sendBeaconinstead)
Browsers are increasingly restricting beforeunload behavior. Some mobile browsers ignore it entirely. Treat it as a best-effort safety net, not a guaranteed gate.
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(), orprompt()(they are blocked). - Most asynchronous operations will be canceled (the page is being destroyed).
- Regular
fetchorXMLHttpRequestcalls 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
}));
});
Adding an unload event listener can disable the back-forward cache in some browsers, making backward navigation slower. Prefer pagehide over unload and pageshow over checking state on load.
// ❌ May disable bfcache
window.addEventListener("unload", () => { ... });
// ✅ bfcache-friendly
window.addEventListener("pagehide", (event) => { ... });
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:
readystatechange(to"interactive")DOMContentLoadedreadystatechange(to"complete")windowload
In practice, readystatechange is rarely used directly. Most developers use DOMContentLoaded and window load events, which are clearer in intent. The readystatechange event is primarily useful when building libraries or utilities that need to work correctly regardless of when they are loaded.
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
readystatechangetransitions fromloadingtointeractivetocompleteDOMContentLoadedfiring afterreadyStatebecomesinteractive- Image
loadevents firing betweenDOMContentLoadedandwindowload windowloadfiring last, after all images are loadedbeforeunloadwith unsaved changes detectionnavigator.sendBeaconfor 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 beforeDOMContentLoaded)"complete": All resources loaded (just beforeload)- Use it to safely initialize code that might run after events have already fired
Key Guidelines:
- Use
DOMContentLoadedfor most initialization (faster thanload). - Use
loadonly when you need fully loaded resources (images, fonts). - Use
beforeunloadsparingly and only for genuine unsaved changes. - Use
navigator.sendBeacon()for reliable data sending during page exit. - Prefer
pagehideoverunloadto preserve back-forward cache compatibility. - Use
document.readyStatechecks to handle late-loading scripts safely.