Not only can you, but you have to make a special effort not to if you don't want to. :-)
When the browser encounters a classic script
tag when parsing the HTML, it stops parsing and hands over to the JavaScript interpreter, which runs the script. The parser doesn't continue until the script execution is complete (because the script might do document.write
calls to output markup that the parser should handle).
That's the default behavior, but you have a few options for delaying script execution:
Use JavaScript modules. A type="module"
script is deferred until the HTML has been fully parsed and the initial DOM created. This isn't the primary reason to use modules, but it's one of the reasons:
<script type="module" src="./my-code.js"></script>
<!-- Or -->
<script type="module">
// Your code here
</script>
The code will be fetched (if it's separate) and parsed in parallel with the HTML parsing, but won't be run until the HTML parsing is done. (If your module code is inline rather than in its own file, it is also deferred until HTML parsing is complete.)
This wasn't available when I first wrote this answer in 2010, but here in 2020, all major modern browsers support modules natively, and if you need to support older browsers, you can use bundlers like Webpack and Rollup.js.
Use the defer
attribute on a classic script tag:
<script defer src="./my-code.js"></script>
As with the module, the code in my-code.js
will be fetched and parsed in parallel with the HTML parsing, but won't be run until the HTML parsing is done. But, defer
doesn't work with inline script content, only with external files referenced via src
.
I don't think it's what you want, but you can use the async
attribute to tell the browser to fetch the JavaScript code in parallel with the HTML parsing, but then run it as soon as possible, even if the HTML parsing isn't complete. You can put it on a type="module"
tag, or use it instead of defer
on a classic script
tag.
Put the script
tag at the end of the document, just prior to the closing </body>
tag:
<!doctype html>
<html>
<!-- ... -->
<body>
<!-- The document's HTML goes here -->
<script type="module" src="./my-code.js"></script><!-- Or inline script -->
</body>
</html>
That way, even though the code is run as soon as its encountered, all of the elements defined by the HTML above it exist and are ready to be used.
It used to be that this caused an additional delay on some browsers because they wouldn't start fetching the code until the script
tag was encountered, but modern browsers scan ahead and start prefetching. Still, this is very much the third choice at this point, both modules and defer
are better options.
The spec has a useful diagram showing a raw script
tag, defer
, async
, type="module"
, and type="module" async
and the timing of when the JavaScript code is fetched and run:
Here's an example of the default behavior, a raw script
tag:
.found {_x000D_
color: green;_x000D_
}
_x000D_
<p>Paragraph 1</p>_x000D_
<script>_x000D_
if (typeof NodeList !== "undefined" && !NodeList.prototype.forEach) {_x000D_
NodeList.prototype.forEach = Array.prototype.forEach;_x000D_
}_x000D_
document.querySelectorAll("p").forEach(p => {_x000D_
p.classList.add("found");_x000D_
});_x000D_
</script>_x000D_
<p>Paragraph 2</p>
_x000D_
(See my answer here for details around that NodeList
code.)
When you run that, you see "Paragraph 1" in green but "Paragraph 2" is black, because the script ran synchronously with the HTML parsing, and so it only found the first paragraph, not the second.
In contrast, here's a type="module"
script:
.found {_x000D_
color: green;_x000D_
}
_x000D_
<p>Paragraph 1</p>_x000D_
<script type="module">_x000D_
document.querySelectorAll("p").forEach(p => {_x000D_
p.classList.add("found");_x000D_
});_x000D_
</script>_x000D_
<p>Paragraph 2</p>
_x000D_
Notice how they're both green now; the code didn't run until HTML parsing was complete. That would also be true with a defer
script
with external content (but not inline content).
(There was no need for the NodeList
check there because any modern browser supporting modules already has forEach
on NodeList
.)
In this modern world, there's no real value to the DOMContentLoaded
event of the "ready" feature that PrototypeJS, jQuery, ExtJS, Dojo, and most others provided back in the day (and still provide); just use modules or defer
. Even back in the day, there wasn't much reason for using them (and they were often used incorrectly, holding up page presentation while the entire jQuery library was loaded because the script
was in the head
instead of after the document), something some developers at Google flagged up early on. This was also part of the reason for the YUI recommendation to put scripts at the end of the body
, again back in the day.