· By John Kavanagh

DOMContentLoaded vs. load in JavaScript

Abstract image used to represent DOMContentLoaded vs. load in JavaScript
Image by Jason Leung.

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:

  1. it downloads and parses HTML
  2. it builds the DOM
  3. it continues loading other resources
  4. 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 DOMContentLoaded when you need the DOM tree
  • use window load when 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

  • DOMContentLoaded fires when the DOM has been built from the HTML.
  • The window load event 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 defer can 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.

Have a complex web platform issue?

Tell me what is blocked, what has changed, and what needs to be true after the fix. I'll come back with a practical next step.