A plain script
The browser's main thread reads index.html from top to bottom. Its HTML parser turns each tag into a node of the DOM, the tree of elements the page is built from. Lines 1 and 2 give html, head and title.
Line 3 is a script element. Its src attribute names a JavaScript file, app.js, kept on the web server. app.js asks the DOM for the first h1 element and prints what it gets to the console.
With no attribute, this is a classic script. The parser stops at line 3 and waits while app.js downloads. Nothing below line 3 is read yet, so the h1 is not in the DOM.
Now the main thread runs app.js. It asks for the h1, but the parser has not reached line 4, so querySelector finds nothing and returns null. The console prints null.
Only now does the parser go on: the h1 and the long paragraph join the DOM. With the whole file read, the browser fires the DOMContentLoaded event, at time 11. The wait for app.js delayed everything below it.
defer
Reload with defer added to the tag. The download now runs in parallel: the parser keeps reading while app.js arrives, so the h1 and paragraph join the DOM without waiting. The script waits its turn.
A deferred script runs once the whole file is parsed, just before DOMContentLoaded. The full DOM exists by then, so app.js finds the h1. Several deferred scripts run in the order they appear.
async and modules
With async, app.js also downloads in parallel. But it runs the moment it arrives, pausing the parser wherever it is. Here it arrives at time 5, in the middle of the long paragraph, and finds the h1.
Because an async script runs whenever it arrives, a fast download could run it before the h1 exists, and several async scripts can run in any order. DOMContentLoaded does not wait for them.
A script with type="module" is a JavaScript module. Modules behave like defer without being asked: they download in parallel and run in order after parsing. Adding async makes a module run as soon as it is ready.
Use defer for scripts that work with the page. Use async for scripts that stand alone, such as analytics. On a classic script, both only work with src: an inline script always runs at once.
What happens
;; step 1/11 · parse-order
file index.html, read top to bottom
parser tag → node in the DOM tree
line 2 <title>Scripts</title> → title node
(html and head are added by the parser)
time parse 0 → 2
The times on the timeline are made-up round numbers. They show the order of events; real times depend on the network and the page. Real browsers also run a "preload scanner" that looks ahead in the file and starts downloads early, even while the parser is blocked. It speeds downloads up but does not change when scripts run.