JavaScript Page Lifecycle Events
Learn the JavaScript page lifecycle events DOMContentLoaded, load, beforeunload, and unload, when each one fires, and how they differ.
Every page a browser opens passes through a predictable sequence of stages: the HTML is parsed, external resources download, the user interacts, and eventually the page is torn down. JavaScript exposes this sequence through four page lifecycle events — DOMContentLoaded, load, beforeunload, and unload. Knowing exactly when each one fires lets you run setup code at the earliest safe moment, defer expensive work until everything is ready, and protect the user from losing unsaved data.
This guide covers what each event means, the order in which they fire, the common gotchas, and the modern replacements for the events that browsers now discourage. Every example below is runnable — paste it into an HTML file or open it in the embedded editor.
The page lifecycle at a glance
The events fire in a fixed order. The first two happen as the page comes alive; the last two happen as the user leaves:
| Event | Target | When it fires | Typical use |
|---|---|---|---|
DOMContentLoaded | document | HTML is fully parsed; scripts run; images/styles may still be loading | Initialize the UI, attach listeners, query the DOM |
load | window | The page and all sub-resources (images, stylesheets, iframes) finished | Read image sizes, run code that needs everything present |
beforeunload | window | The user is about to navigate away | Warn about unsaved changes |
unload | window | The page is being torn down (deprecated) | Legacy cleanup / analytics |
A key takeaway: DOMContentLoaded is the event you want most of the time. It fires far earlier than load, so attaching your code to it makes the page interactive sooner.
Deep Dive into the DOMContentLoaded Event
The DOMContentLoaded event fires as soon as the HTML document has been fully parsed, which typically occurs long before images, stylesheets, and other external resources have loaded. At this point the DOM tree is complete, so document.getElementById, querySelector, and friends will find every element written in the HTML.
Because it does not wait for images or CSS, you should use DOMContentLoaded whenever your script only needs the DOM structure — which is the overwhelmingly common case.
How scripts affect DOMContentLoaded
The browser pauses HTML parsing whenever it meets an ordinary <script> tag, runs the script, and only then continues. That means DOMContentLoaded waits for synchronous scripts. There are two important nuances:
<script async>runs as soon as it downloads and does not blockDOMContentLoaded.<script defer>runs after the document is parsed but beforeDOMContentLoadedfires, in document order.
If you control script timing with these attributes, see scripts: async, defer for the full picture.
Checking the state with document.readyState
If your code might run after the DOM is already parsed (for example, a script appended dynamically), the listener would never fire because the event already happened. Guard against it with document.readyState:
function onReady() {
console.log('DOM is ready, current state:', document.readyState);
}
if (document.readyState === 'loading') {
// Still parsing — wait for the event.
document.addEventListener('DOMContentLoaded', onReady);
} else {
// 'interactive' or 'complete' — DOM is already ready.
onReady();
}readyState moves through three values: "loading" while parsing, "interactive" once the DOM is ready (this is when DOMContentLoaded fires), and "complete" once everything including resources has loaded (this is when load fires).
Interactive Example: DOMContentLoaded in Action
To see DOMContentLoaded in action, the following example updates the content of a div element once the HTML document is fully parsed but before the entire page is fully loaded.
<!DOCTYPE html>
<html lang="en">
<head>
<title>DOMContentLoaded Example</title>
</head>
<body>
<div id="output">Waiting for DOM...</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('output').innerHTML = 'DOM fully loaded and parsed!'
});
</script>
</body>
</html>This example can be pasted into any HTML file and viewed in a browser to observe how quickly DOMContentLoaded triggers compared to the full page load.
Exploring the load Event
The load event is essential for operations that require the entire web page to be fully loaded, including all dependent resources like images and stylesheets. By the time load fires, every image has its real dimensions, fonts are applied, and iframes are loaded — so this is the right place for code that measures the rendered page.
The trade-off is timing: a single large image or a slow third-party widget delays load for the whole page. That is why you should reserve load for work that genuinely depends on resources, and keep ordinary initialization on DOMContentLoaded. For loading and error handling of individual resources (rather than the whole page), see resource loading: onload and onerror.
Interactive Example: Demonstrating the load Event
Here, we create an example where a message is displayed only after everything on the page is completely loaded. As you can see, the DOMContentLoaded event always happens before the load event.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Load Event Example</title>
</head>
<body>
<div>You see first triggered event on top of the other.</div>
<script>
window.addEventListener('load', function() {
const newDiv = document.createElement("div");
newDiv.innerHTML = 'loaded event happened!'
document.body.append(newDiv);
});
window.addEventListener('DOMContentLoaded', function() {
const newDiv = document.createElement("div");
newDiv.innerHTML = 'DOMContentLoaded event happened!'
document.body.append(newDiv);
});
</script>
</body>
</html>Copy and test this code in your own HTML environment to see the difference in timing between DOMContentLoaded and load.
Handling the beforeunload Event
The beforeunload event is incredibly useful for preventing data loss, warning users before they navigate away from a page that may have unsaved changes. When a handler sets event.returnValue, the browser shows a generic confirmation dialog ("Leave site? Changes you made may not be saved").
A few rules browsers now enforce, worth knowing before you rely on this:
- The custom message is ignored. To stop spam, browsers display their own text regardless of the string you assign. Setting
returnValueto any non-empty value (or callingevent.preventDefault()) is enough to trigger the prompt. - The dialog only appears if the user has interacted with the page (clicked, typed, etc.). A page the user never touched cannot block navigation.
- Only register the handler when there are actually unsaved changes, and remove it once data is saved. A permanent
beforeunloadhandler annoys users and can hurt back/forward cache performance.
A more robust pattern adds and removes the listener based on a dirty flag:
let isDirty = false;
function beforeUnloadHandler(event) {
event.preventDefault();
// Required for some browsers to show the dialog.
event.returnValue = '';
}
function markDirty() {
if (!isDirty) {
isDirty = true;
window.addEventListener('beforeunload', beforeUnloadHandler);
}
}
function markSaved() {
isDirty = false;
window.removeEventListener('beforeunload', beforeUnloadHandler);
}Interactive Example: Implementing beforeunload Confirmation
This example prompts the user with a confirmation dialog when attempting to leave the page, helping prevent accidental data loss.
<!DOCTYPE html>
<html lang="en">
<head>
<title>beforeunload Example</title>
</head>
<body>
<p>Now if you want to exit this page, a confirmation alert will be shown as a result of the <code>beforeunload</code> event.</p>
<script>
window.addEventListener('beforeunload', function(event) {
event.returnValue = '';
});
</script>
</body>
</html>Test this code by entering the "try it yourself" page and then trying to navigate to another page.
Utilizing the unload Event
The unload event is deprecated and you won't be able to use it in most modern browsers. It is also considered a bad practice. Therefore this part is only for better information about legacy codes.
Though less commonly used due to modern browser restrictions and the rise of single-page applications, the unload event historically ran as a page was being torn down — useful for cleaning up resources or sending a final analytics ping. The problem is that unload (and beforeunload) prevents the browser from storing the page in the back/forward cache (bfcache), which makes back-button navigation slower for everyone.
Modern alternatives: visibilitychange and pagehide
Instead of unload, listen for visibilitychange and treat the transition to "hidden" as the last reliable moment to persist state. This fires consistently on desktop and mobile, including when the user switches tabs or closes the app:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// Last reliable point to save state.
console.log('Page hidden — flush analytics / save draft here');
}
});For sending data on exit, use navigator.sendBeacon(), which queues a request that survives the page being closed:
function sendStats(data) {
navigator.sendBeacon('/log', JSON.stringify(data));
}Interactive Example: Using the unload Event
This code shows an alert message when the page is being unloaded. It is deprecated as mentioned, and it does not work in most modern browsers — prefer visibilitychange above.
<!DOCTYPE html>
<html lang="en">
<head>
<title>unload Example</title>
</head>
<body>
<script>
window.addEventListener('unload', function(event) {
alert('Page is unloading...');
// Perform cleanup tasks or analytics here
});
</script>
</body>
</html>Conclusion
Each of these events serves a specific phase in the lifecycle of a webpage, from initial loading to final unloading. The practical guidance is short:
- Run most initialization on
DOMContentLoaded— it fires earliest and the DOM is ready. - Use
loadonly when you depend on images, fonts, or other sub-resources being fully present. - Use
beforeunloadto guard unsaved changes, and add the listener only while data is dirty. - Avoid
unload; reach forvisibilitychangeplusnavigator.sendBeacon()instead.
To go deeper, explore these related chapters:
- Introduction to browser events
- Scripts: async, defer
- Resource loading: onload and onerror
- Browser environment and specs