app.jsDOM TREEhtmlheadbodytitlescripth1pTIMELINEclassicnetdeferasync01234567891011timeparse HTMLrun JSdownloadwaitingDCLMAIN THREADconsole1<!DOCTYPE html>2<title>Scripts</title>3<script src="app.js"></script><script defer src="app.js"></script><script async src="app.js"></script><script type="module" src="app.js"></script>4<h1>Hello</h1>5<p>A long article...</p>1let h = document.querySelector("h1");2console.log(h);app.js<title><h1>null<h1>
Front: index.html (line 3 is the script tag) and app.jsBack: the server, the DOM tree and the timeline (one row per kind of script)Chips: tags joining the DOM, app.js downloading, and what it logsFront left: index.html, whose line 3 loads app.js (front right). Middle: the browser's main thread and the console. Back: the web server, the DOM tree the parser builds, and a timeline with one row per kind of script. Each row shows the main thread (parse HTML, wait, run JavaScript) and the download of app.js as a clock moves; the app.js chip travels while its download bar grows. A triangle marks DOMContentLoaded (DCL). 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.

A plain script

  1. 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.

defer

async and modules

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.