DOMContentLoaded vs. load in JavaScript

In Brief
DOMContentLoaded fires after HTML parsing and deferred scripts. Use it when your code needs the DOM, and check document.readyState if the event may already have fired. The window load event waits for more page resources, but does not prove that each one succeeded. Track an individual resource separately when your feature depends on it.
One of the first timing problems new JavaScript developers hit is painfully ordinary: the code runs, the selector looks right, and yet the element is missing because the page is not ready when the script executes.
That usually sends people looking for some vague "run this later" solution, and they quickly stumble into two browser events: DOMContentLoaded and load.
At first glance they sound interchangeable. They are not.
The difference matters because they fire at different points in the browser's page‑loading process, and using the wrong one can make your code run either too early or much later than necessary.
DOMContentLoaded fires when the HTML has been parsed
DOMContentLoaded fires when the browser has finished parsing the HTML document and built the DOM tree.
That means:
- the document structure exists
- elements can be queried
- you can attach event listeners
- you can update text, classes, and attributes
It does not mean every external resource has finished loading.
Images, stylesheets, iframes, and other assets may still be loading when DOMContentLoaded fires. The key point is that the document structure is ready to work with.
That makes it the right event for front‑end behaviour surprisingly often.
function initialiseSaveButton() {
var button = document.querySelector('.save-button');
if (!button) {
return;
}
button.addEventListener('click', function () {
console.log('Saved');
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialiseSaveButton);
} else {
initialiseSaveButton();
}The document.readyState check handles both script timings. Whilst the document is still loading, wait for DOMContentLoaded; otherwise, the DOM has already been parsed and the setup can run immediately. Attaching only a listener after the event has fired would leave the button unwired.
load waits for much more
The load event fires later.
The window load event waits for the resources participating in the page load, including the usual images, stylesheets, scripts, and frames. It is a completion milestone, not proof that every resource loaded successfully.
- images
- stylesheets
- iframes
- scripts loaded in the normal way
That means load is a heavier milestone. If you only need the DOM, waiting for load can be unnecessary.
window.addEventListener('load', function () {
console.log('The page load event has fired');
});That can be useful in some cases, but it is not the best default for ordinary UI setup.
Why New Developers Confuse Them
The confusion usually comes from thinking of page load as one single moment.
It is not. The browser moves through stages:
- it downloads and parses HTML
- it builds the DOM
- it continues loading other resources
- it eventually reaches a fully loaded state
DOMContentLoaded lives closer to stage two.
load lives closer to stage four.
If you are wiring up buttons, menus, tabs, forms, or text changes, stage two is normally enough. Waiting all the way to stage four can make the page feel more sluggish for no good reason.
A Practical Example
Imagine a page containing this markup:
<button class="menu-toggle">Menu</button>
<img src="/hero.jpg" alt="" />If your goal is simply to attach a click handler to the button, you do not need to wait for the image to download first.
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.querySelector<HTMLButtonElement>('.menu-toggle');
toggle?.addEventListener('click', () => {
console.log('Open menu');
});
});Using load there would still work, but it would delay the setup until after the image and other resources had finished loading. That is later than necessary.
When load is actually the better choice
load makes more sense when your code depends on resources beyond the raw DOM structure.
For example:
- measuring image dimensions after the image has loaded
- running logic that depends on all styles being applied and assets available
- coordinating with iframes or resource‑heavy widgets
If the behaviour depends on one particular image, listen for that image's load and error events and handle an image that has already completed. Waiting for the window event alone cannot tell you whether that image succeeded.
So the question should not be "which event is best?" It should be "what exactly am I waiting for?"
Script Placement Also Changes the Picture
Before reaching for either event, it is worth remembering that script placement matters too.
If your script tag sits at the end of the body, the browser has already parsed the earlier HTML by the time the script runs.
<body>
<button class="menu-toggle">Menu</button>
<script src="/app.js"></script>
</body>In setups like that, your code may be able to query the DOM immediately without wrapping everything in DOMContentLoaded.
Likewise, a script with the defer attribute waits until after the document has been parsed before it executes:
<script src="/app.js" defer></script>That means some older "wrap absolutely everything in DOMContentLoaded" advice becomes less necessary once your script loading strategy is sensible.
DOMContentLoaded is about structure, not visual completeness
This is another useful distinction.
If a page has fired DOMContentLoaded, it does not mean the page looks complete yet. It means the document exists and can be manipulated.
That is a much more useful milestone for JavaScript than "everything the user can eventually see is fully downloaded". Many front‑end behaviours only care that the structure is available.
Beginners sometimes wait for load because it feels safer. In reality, that often just hides a fuzzy mental model of what the code actually depends on.
Browser Timing Bugs Often Come from Using the Wrong Milestone
If your code is failing because elements are missing, your problem is often that you are running before the DOM is ready.
If your code is running much later than you expect, your problem may be that you are waiting for load when only DOMContentLoaded was needed.
Both events are worth understanding properly. They are both valid. They just answer different timing questions.
A Useful Rule of Thumb
For ordinary front‑end development:
- use
DOMContentLoadedwhen you need the DOM tree - use window
loadwhen you need the broader page‑load milestone, with separate success or failure handling for resources the feature depends on
That simple rule is not perfect in every edge case, but it is reliable enough for most day‑to‑day work.
Wrapping Up
DOMContentLoaded and load answer different timing questions. The former is usually enough for wiring up the DOM. The latter waits for the broader page load, but a specific resource can still have failed. Choose the event that matches what the feature actually needs.
Key Takeaways
DOMContentLoadedfires when the DOM has been built from the HTML.- The window
loadevent waits for the resources participating in page loading, but does not guarantee they all succeeded. - Most JavaScript UI setup only needs
DOMContentLoaded. - Script placement and
defercan remove the need for extra readiness wrappers in some cases. - The right event depends on what your code is truly waiting for.
Once you stop treating page readiness as one vague moment, these events become much easier to choose between.
Postscript
April 2026: During restoration, some examples were rewritten with modern JavaScript and TypeScript, including optional chaining from TypeScript 3.7. Those examples post‑date the original April 2014 article. In modern browsers, window load also excludes lazily loaded resources. For a specific image, use its load and error events, or HTMLImageElement.decode() when the work must wait for decoding.