async & defer
What happens by default when the browser's HTML parser hits a <script> tag, and how the async and defer attributes each change when a script downloads and runs — so you know which one to reach for.
The browser parses HTML like it's reading a script out loud
Picture the browser parsing HTML like someone reading a script out loud, line by line, top to bottom, acting on each tag as it goes: hit an <img>, fetch the image; hit a <link rel="stylesheet">, fetch the CSS. That "read and act as you go" process is HTML parsing.
<script> tags break this pattern. Because JavaScript can rewrite the HTML directly, the browser can't just assume "skip this for now, run it later, no harm done." So the default behavior is deliberately conservative: parsing stops dead, the script downloads, runs to completion, and only then does parsing pick back up.
<p>First paragraph</p>
<script src="heavy-script.js"></script>
<p>Second paragraph</p>Here, the browser reads the first <p>, hits the <script> tag, and just stops: it fires off a request for heavy-script.js, waits for it to download and run, and only then moves on to the second <p>. If heavy-script.js is large or the server is slow, the page visibly stalls — the second paragraph just doesn't show up. This "blocks both download and execution" behavior is how <script> tags worked from the start, and it's exactly the problem async and defer were built to solve.
async: downloads in the background, but jumps the queue to run
Add the async attribute and the browser pushes the download to the background instead of blocking the parser — parsing just keeps going. That alone fixes the "download stalls the page" problem.
<p>First paragraph</p>
<script async src="heavy-script.js"></script>
<p>Second paragraph</p>But here's the catch: async only changes when the download happens, not when the script runs. The moment it finishes downloading, the browser pauses whatever parsing it's doing, cuts in line to run the script immediately, and only resumes parsing once it's done. If the download is fast enough, that interruption could happen right after the browser reads the first <p> — there's no way to predict exactly where in the parse it'll cut in.
With multiple async scripts on a page, this "whoever downloads first runs first" behavior gets even more obvious: even if a.js appears before b.js in the HTML, if b.js is smaller and finishes downloading first, it runs first — execution order has nothing to do with document order anymore.
<script async src="a.js"></script>
<script async src="b.js"></script>
<!-- if b.js finishes downloading first, it runs before a.js -->That's why async is the right call for scripts with no dependency on anything else on the page — analytics, ad scripts, anything where it genuinely doesn't matter which one runs first.
defer: downloads in the background, and waits its turn to run
defer also pushes the download to the background without blocking parsing — same as async so far. The difference is in when it runs: a deferred script never cuts in line, even if it finishes downloading early. It waits until the entire HTML document has been parsed, and scripts are guaranteed to run in the order they appear in the document.
<script defer src="a.js"></script>
<script defer src="b.js"></script>
<!-- regardless of download order, a.js always runs before b.js -->That "wait for parsing to finish, keep the order guaranteed" behavior lines up almost perfectly with what most application code actually needs. Your code probably calls querySelector to grab DOM elements — if the DOM tree isn't fully parsed yet, that query can come back null. Your scripts probably depend on each other too, like b.js using a variable defined in a.js — get the order wrong and it just throws. Because defer runs after parsing finishes but before the DOMContentLoaded event fires, you can safely assume the DOM is fully ready by the time a deferred script executes.
A quick comparison
| Syntax | Blocks parsing during download? | When it runs | Execution order |
|---|---|---|---|
<script src="..."> | Yes | Immediately after download, blocks parsing | Document order |
<script async src="..."> | No | Immediately after download, cuts in line | Whoever downloads first |
<script defer src="..."> | No | After parsing finishes | Document order |
Which one should you actually use?
- No DOM dependency, doesn't care about order relative to other scripts: use
async. Think analytics (Google Analytics and friends), ad scripts — code that runs independently, where finishing sooner is better and cutting in line half a second early doesn't affect anything else on the page. - Touches the DOM, or depends on other scripts: use
defer. This covers most of your application's main logic — say, a utility library loaded before the business logic that depends on it. - Writing ES modules:
<script type="module">already behaves likedeferby default — it doesn't block parsing and waits until the HTML is fully parsed to run. No need to adddeferon top.
Takeaway
By default, <script> tags block both download and execution during HTML parsing — a conservative choice left over from a time when scripts could rewrite the page via document.write(). async unblocks the download but still cuts in line to run as soon as it's ready, with no guaranteed order — good for independent third-party scripts. defer also unblocks the download, but waits for parsing to finish and runs scripts in document order — good for application code that touches the DOM or depends on other scripts. Next time you're staring at a <script> tag wondering whether to add an attribute, just ask: does this code care about execution order relative to the DOM or other scripts? That answer tells you whether you want async or defer.