Document Ready & DOMContentLoaded

jQuery · Lesson 3 of 24 6 min read Updated

A script can only change an element that already exists. If your code runs before the browser has read the HTML for a button, the button is not there yet, and your click handler is attached to nothing. This is one of the most common reasons a beginner's jQuery "does nothing".

In this lesson you will see the problem happen, fix it the jQuery way with a ready handler, and then fix it the modern way with defer, modules and DOMContentLoaded.

Why timing matters

The browser reads HTML from top to bottom. When it meets a normal <script>, it stops, runs the script, and only then continues reading. So a script in <head> runs while <body> is still empty.

Run this page. Both scripts are in <head>, but only the second one waits:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Too early</title>
  <script src="https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js"></script>
  <script>
    // Runs immediately: the paragraphs do not exist yet
    $("#first").text("Changed without waiting");
    console.log("Elements found without waiting:", $("#first").length);

    // Runs once the whole HTML has been read
    $(function () {
      $("#second").text("Changed inside a ready handler");
      console.log("Elements found inside ready:", $("#second").length);
    });
  </script>
</head>
<body>
  <p id="first">Original text</p>
  <p id="second">Original text</p>
</body>
</html>

The first paragraph never changes, and there is no error. When a jQuery selector matches nothing, you get an empty jQuery object, and calling .text() on an empty set quietly does nothing. That silence makes the bug hard to spot. The console line shows 0 elements found, which is your clue.

The jQuery ready handler

Passing a function to $ registers a ready handler: a function jQuery runs as soon as the HTML has been fully parsed (read and turned into the DOM), without waiting for images.

JavaScript
$(function () {
  // Safe: every element in the HTML exists now
  $("#menu-toggle").on("click", () => $("#menu").toggleClass("open"));
});

A few useful facts about ready handlers:

  • You can register as many as you like; they run in the order you added them.
  • If the page is already ready when you register one (for example in a script loaded later), it still runs.
  • Since jQuery 3.0 a ready handler always runs asynchronously: the code after $(function () {...}) finishes first, then the handler runs.
  • The handler receives jQuery as its first argument, which is why jQuery(function ($) { ... }) works in no-conflict pages such as WordPress.

Ready versus load

"Ready" means the HTML is parsed. Load means everything has finished downloading too: images, stylesheets and iframes. Wait for load only when you need something like an image's real size.

This page records when each event fires:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Ready vs load</title>
  <script src="https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js"></script>
  <script>
    const start = performance.now();
    const log = (label) => console.log(label + " after " + Math.round(performance.now() - start) + " ms");

    document.addEventListener("DOMContentLoaded", () => log("DOMContentLoaded (plain JS)"));
    $(function () { log("jQuery ready handler"); });
    $(window).on("load", function () {
      log("window load (jQuery)");
      $("#size").text("The photo is " + $("#photo").prop("naturalWidth") + " pixels wide.");
    });
  </script>
</head>
<body>
  <img id="photo" src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills at sunrise" width="400">
  <p id="size">Waiting for the image...</p>
</body>
</html>

Both "ready" lines appear before the load line. The image width is only reliable inside the load handler, because only then has the image file arrived.

The modern way: defer and modules

Modern pages usually avoid ready handlers altogether by loading scripts so that they run after parsing:

HTML
<!-- Option A: defer (runs in order after the HTML is parsed) -->
<script src="js/app.js" defer></script>

<!-- Option B: a module script is deferred automatically -->
<script type="module" src="js/app.js"></script>

<!-- Option C: put the script at the end of <body> -->

With any of these, the code inside app.js can select elements directly, with no wrapper function. This also works for jQuery code: if jQuery and app.js are both deferred, app.js can call $("#menu") straight away.

When you cannot control how a script is loaded, use the DOMContentLoaded event. There is one trap: unlike jQuery's ready handler, a DOMContentLoaded listener added after the event has fired never runs. Check document.readyState first:

JavaScript
function onReady(callback) {
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", callback, { once: true });
  } else {
    callback(); // the HTML is already parsed
  }
}

onReady(() => {
  document.querySelector("#status").textContent = "Page ready";
});

document.readyState is "loading" while the HTML is still being read, then "interactive" (parsed) and finally "complete" (everything loaded).

GoaljQueryPlain JavaScript
Run after HTML is parsed$(function () { ... })defer, type="module", or DOMContentLoaded
Run after images too$(window).on("load", fn)window.addEventListener("load", fn)
Late registration still runsYes, automaticallyOnly if you check document.readyState

Common mistakes

  • Selecting elements in <head> without waiting. The selection is empty and nothing happens, with no error message.
  • Wrapping every file in its own ready handler "just in case". If the script is deferred or a module, the wrapper adds nothing. Choose one approach per project.
  • Using $(window).on("load") for everything. It waits for every image and makes your page feel slow. Use ready unless you really need loaded assets.
  • Attaching a load handler inside a ready handler. If the page has already loaded by then, the load handler never runs.

Key points

  • Code must wait until the elements it uses have been parsed.
  • jQuery's ready handler is $(function () { ... }); $(document).ready(...) is the older spelling.
  • Ready handlers run after parsing (not after images), run asynchronously, and still run if added late.
  • Modern code uses defer, type="module" or a script at the end of <body> instead of a wrapper.
  • DOMContentLoaded does not fire for listeners added late, so check document.readyState.

Exercise

The script below sits in <head> and does nothing. Fix it twice: first using a jQuery ready handler, then by rewriting it as plain JavaScript in a separate file loaded with defer.

$("#greeting").text("Namaste, Rohan!");

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Greeting</title>
  <script src="https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js"></script>
  <script>
    $(function () {
      $("#greeting").text("Namaste, Rohan!");
    });
  </script>
</head>
<body>
  <p id="greeting">Hello</p>
</body>
</html>

For the plain version, save document.querySelector("#greeting").textContent = "Namaste, Rohan!"; in greeting.js and load it with <script src="greeting.js" defer></script>; defer makes it run after the HTML is parsed, so no wrapper is needed.

Quick Quiz

1. What happens when you call $("#price").text("₹99") before the element exists?

2. Which is the recommended way to write a jQuery ready handler in jQuery 3 and 4?

3. A script has type="module". Does it need a ready handler to find elements in the HTML?

4. Why should you check document.readyState before adding a DOMContentLoaded listener?

Learn jQuery with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call