completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

From HTML to running script

Learn how browsers tokenize HTML, build a document, discover scripts, fetch them, and choose when each kind may execute.

By the end, you can
  • 01
    Follow the parserExplain how HTML becomes tokens and then elements, and why a plain script makes the parser wait.
  • 02
    Choose how a script loadsCompare classic, async, defer, and module scripts: when each one downloads, when it runs, and in what order.
  • 03
    Explain loading surprisesExplain how the preload scanner finds files early, why document.write breaks easily, and how the lesson's model differs from real download times.

HTML arrives before JavaScript runs

A web page arrives as HTML text, a little at a time. The browser doesn't wait for all of it. It starts reading as soon as the first part arrives.

The part of the browser that reads HTML is called the HTML parser. Reading the HTML is called parsing. The parser turns the text into page elements, such as paragraphs and buttons.

A <script> tag can sit anywhere in that HTML. So a script may run before the whole page exists.

Definition

Script loading is everything that happens to a script tag, in this order:

  1. The browser finds the tag.
  2. It downloads the script's file, if it has one.
  3. It decides when the script may run.
  4. It runs the script.
  5. It carries on reading the HTML.

The tag itself decides when its code runs. These things change the timing:

  • where the tag sits in the HTML;
  • src, which names a separate file to download;
  • the words async and defer on the tag;
  • type="module" on the tag.

You'll meet each one in this lesson. For every script, keep three questions apart:

  • Where has the parser got to?
  • Has the script's file finished downloading?
  • Is the script allowed to run yet?

These are different questions, even when the answers seem to come at the same moment.

This lesson continues from What happens when you navigate.

The HTML tokenizer and tree builder

The parser does its work in two parts:

  • The tokenizer reads the HTML one character at a time. It cuts the text into pieces called tokens. A token is a start tag like <p>, some text, or an end tag like </p>.
  • The tree builder takes the tokens one by one. It turns them into elements and puts each element inside its parent. The result is the document tree, also called the DOM.

Both parts work while the HTML is still arriving. The browser can make the first paragraph before the last part of the page arrives. That is why a script can find an element that comes earlier in the HTML.

The code below is a small model of these two parts. It is a teaching model written for this lesson, not the browser's real code. The lesson's setup code defines tokenizeTinyHtml and buildTinyTree for you.

A tiny model: HTML to tokens to a treePop out in the code editor (opens in a new tab)JavaScript
const markup = "<p>Hi <b>there</b></p>";
const tokens = tokenizeTinyHtml(markup);
const tree = buildTinyTree(tokens);

console.log(tokens.map((token) => token.type).join(","));
console.log(tree.children[0].children.map((node) => node.name).join(","));

Line 1 stores a short piece of HTML. Line 2 cuts it into tokens, in the order they appear. Line 3 gives those tokens to the tree builder. Line 5 prints the kind of each token: start-tag,text,start-tag,text,end-tag,end-tag.

The HTML <p>Hi <b>there</b></p> goes through the tokenizer and comes out as six tokens, left to right: <p> start tag, Hi text, <b> start tag, there text, </b> end tag, </p> end tag.<p>Hi <b>there</b></p>tokenizer<p>start tag"Hi "text<b>start tagtheretext</b>end tag</p>end tag
The tokenizer reads the HTML from left to right and cuts it into six pieces, called tokens. The tree builder then uses these tokens to build the page's elements.

Line 6 prints "Hi ",b. These are the two things directly inside the paragraph: first the text, then the bold element b. The word there is inside b. So it is not directly inside the paragraph.

Step through: HTML to tokens to a tree
Step 0 of 7Ready
Your turn: follow the blue line

Step through a small model of the tokenizer and the tree builder. Every value comes from real calls to this lesson's code. It is not the browser's own code.

Running in
  1. script
Next: line 1
const tokens = tokenizeTinyHtml(markup);const tree = buildTinyTree(tokens); console.log(tokens.map((token) => token.type).join(","));console.log(tree.children[0].children.map((node) => node.name).join(","));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Browsers also fix broken HTML. In the code below, the first paragraph has no closing </p> tag. The model follows one real browser rule: a new <p> closes the paragraph that is still open.

A missing closing tag still gives two paragraphsPop out in the code editor (opens in a new tab)JavaScript
const tree = buildRecoveringTree("<p>Tea<p>Cake");
const paragraphs = tree.children.filter((node) => node.name === "p");

console.log(paragraphs.length);
console.log(paragraphs.map((node) => node.children[0].name).join(","));

Line 1 leaves out the first closing tag on purpose. Line 2 picks out the paragraphs from the tree. Line 4 prints 2, and line 5 prints "Tea","Cake". Real browsers have many more rules like this one. The model shows only one.

Scripts that make the parser wait

Two words first:

  • A classic script is a plain <script> tag, with no type="module".
  • An inline script has its code written inside the tag, with no src file.

An inline classic script runs the moment the parser reaches it. It can find elements the parser has already built. It can't find elements that come later in the HTML.

A script sees only the paragraph before itPop out in the code editor (opens in a new tab)HTML
<p>Hello</p><script>console.log(document.querySelectorAll("p").length)</script><p>World</p>

The parser reads the first paragraph before the script. The script calls querySelectorAll("p"), which finds every paragraph on the page so far. Only the first paragraph exists, so it prints 1. Then the parser carries on and builds the second paragraph.

Three cards from top to bottom: <p>Hello</p>, already built; <script>, where the parser is now; <p>World</p>, not read yet. An arrow from the script card points to the output 1.<p>Hello</p>already built<script>parser is here<p>World</p>not read yet1prints
The parser reads the page from top to bottom. When it reaches the script, only the first paragraph exists, so the script counts one paragraph and prints 1.

A classic script with a src file follows the same rule, plus a download. The parser stops while the file downloads. Then it stops again while the script runs. This is called a parser-blocking script, because it blocks (stops) the parser.

A stylesheet can make the wait longer. The script might ask for the page's styles, so the browser may wait for the stylesheet before it runs the script.

Real-life analogyReading a recipe aloud

You read a recipe aloud. You reach the line "go and buy milk". Everything stops until the milk arrives. Meanwhile, a friend can read ahead and start buying the other things early.

With async, you use the milk as soon as it arrives. With defer, you use it only after the whole recipe is read, in the order the recipe lists things.

In real life: You reach the line "go and buy milk"
In JavaScript: The parser reaches a classic script
In real life: Reading stops until the milk arrives
In JavaScript: Parsing waits for the download and the run
In real life: A friend reads ahead and buys other things early
In JavaScript: The preload scanner starts later downloads early (next section)
In real life: Use the milk as soon as it arrives, or after the whole recipe is read
In JavaScript: async, or defer

The preload scanner looks ahead

While the parser is stopped, it can't see the tags further down the page. So the browser has a helper called the preload scanner. It skims ahead through the HTML text. When it sees a file address, it starts downloading that file early, before the parser gets there.

The preload scanner doesn't build elements. It never runs the page's JavaScript. It can only find addresses written plainly in the HTML, such as a stylesheet, an image, or a script's src.

A script address written in the HTMLPop out in the code editor (opens in a new tab)HTML
<script src="/checkout.js" defer></script>

The address /checkout.js is written in the HTML, so the browser can start downloading it early. The defer on the tag still makes the script wait to run until all the HTML is read.

Written in the HTML, or made later by JavaScriptPop out in the code editor (opens in a new tab)JavaScript
const html = '<script src="/app.js" defer></script>';
const injectedLater = 'document.createElement("script")';

console.log(html.includes("src=") ? "found early" : "not found");
console.log(injectedLater.includes("src=") ? "found early" : "created later");

Line 1 is HTML with a src in it, so line 4 prints found early. Line 2 stands for JavaScript that makes a script tag later, with document.createElement("script"). That text has no address in it, so line 5 prints created later.

The preload scanner can't find a script that doesn't exist yet. That script only appears when the JavaScript that makes it runs.

async, defer, and module scripts

Three choices change when a script runs. With all three, the file downloads while the parser keeps reading.

async runs the script as soon as its file has downloaded. This suits a script that no other script depends on. Two async scripts don't keep their written order. A later tag runs first if its file arrives first.

defer runs the script after the parser has read all the HTML. Deferred classic scripts run in the order they are written. So they suit files that must run in order, such as a cart file and then a checkout file.

Two timelines. async: the file downloads while the browser reads the HTML; reading stops for a moment while the script runs, then goes on. defer: the file downloads while the browser reads the HTML; the script runs only after all the HTML is read.asyncread HTMLrunread HTMLdownloaddeferread HTMLrundownloadtime
Both download the file while the browser keeps reading the HTML. An async script runs as soon as its file arrives. A defer script waits until all the HTML is read.

A module script has type="module". It can use import to load other files. The browser downloads the module and every file it imports while the parser keeps reading. Without async, a module waits like defer. With async, it runs as soon as it and all its imports are ready.

An import map is a <script type="importmap"> tag. It tells the browser which file address to use for a short module name, such as "cart". Put it before the modules that use those names. It doesn't run any code itself.

Four tags, four timing choicesPop out in the code editor (opens in a new tab)JavaScript
const tags = [
  '<script src="/app.js"></script>',
  '<script src="/stats.js" async></script>',
  '<script src="/boot.js" defer></script>',
  '<script type="module" src="/main.js"></script>',
];

console.log(tags.length);
console.log("async runs when ready; defer waits for parsing");

Lines 1 to 6 make a list of four script tags:

  • Line 2 is a plain classic script.
  • Line 3 is a classic script with async.
  • Line 4 is a classic script with defer.
  • Line 5 is a module script.

Line 8 prints 4, the number of tags. Line 9 prints the main difference: async runs when ready, and defer waits for parsing.

Each row of the table below is one kind of script tag. Read it as: "this kind of tag treats the parser like this, and this decides its order."

When each kind of script tag runs
Kind of script tagWhat happens to parsing, and when it runsWhat decides the order
Classic script with srcParsing stops while the file downloads and runs. It runs at its spot in the HTML.Its spot in the HTML.
Classic script with asyncThe file downloads while parsing goes on. It runs as soon as the file is ready.Which file finishes downloading first.
Classic script with deferThe file downloads while parsing goes on. It runs after parsing ends.The written order of the tags.
Module without asyncThe module and its imports download while parsing goes on. It runs after parsing ends.The written order of the tags.
Module with asyncThe module and its imports download while parsing goes on. It runs as soon as they are all ready.Which one is ready first, not the written order.

Script execution order

No single rule decides the order scripts run in. Three things decide it together:

  • where the parser has got to;
  • which files have finished downloading;
  • whether a script may run before parsing ends.

Real download times change from one visit to the next. So this lesson uses a small model where you set the times yourself. Each script has a kind and a downloadDoneAt number: the time its file finishes downloading.

In the model, parsing ends at time 3. Deferred scripts and modules run together, once parsing has ended and all of their files have downloaded. This is a teaching model, not the browser's real scheduler.

A small model of the order scripts run inPop out in the code editor (opens in a new tab)JavaScript
const scripts = [
  { name: "inline settings", kind: "inline", downloadDoneAt: 0 },
  { name: "checkout", kind: "async", downloadDoneAt: 2 },
  { name: "app", kind: "defer", downloadDoneAt: 5 },
  { name: "catalog", kind: "module", downloadDoneAt: 1 },
];

console.log(executionOrder(scripts).join(" -> "));

Lines 2 to 5 list four scripts, in the order they appear in the HTML:

  • Line 2 is inline, so it runs in place.
  • Line 3 is async. Its file is ready at time 2.
  • Line 4 is deferred. Its file is ready at time 5.
  • Line 5 is a module. Its file is ready at time 1, but it still waits for parsing.

Line 8 prints inline settings -> checkout -> app -> catalog. The inline script runs first, in place. The async script runs at time 2, as soon as it is ready. The deferred script and the module wait for parsing to end. Then they run in their written order.

Four cards in the order they run: inline settings, runs in place; checkout, async, ready at 2; app, defer, after parsing; catalog, module, after parsing.inline settingsinlineruns in placecheckoutasyncready at 2appdeferafter parsingcatalogmoduleafter parsing
The model prints this order. app and catalog wait until parsing ends, then run in the order they were written, even though catalog was ready first.
Step through: the order scripts run in
Step 0 of 6Ready
Your turn: follow the blue line

Step through a small model of the order scripts run in. Real download times change from one visit to the next. So in this model, you set the times yourself.

Running in
  1. script
Next: line 1
  { name: "inline settings", kind: "inline", downloadDoneAt: 0 },  { name: "checkout", kind: "async", downloadDoneAt: 2 },  { name: "app", kind: "defer", downloadDoneAt: 5 },  { name: "catalog", kind: "module", downloadDoneAt: 1 },]; console.log(executionOrder(scripts).join(" -> "));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Playground: change when the async file finishes downloading
A small model of the order scripts run inJavaScript
const scripts = [  { name: "inline settings", kind: "inline", downloadDoneAt: 0 },  { name: "checkout", kind: "async", downloadDoneAt: 2 },  { name: "app", kind: "defer", downloadDoneAt: 5 },  { name: "catalog", kind: "module", downloadDoneAt: 1 },]; console.log(executionOrder(scripts).join(" -> "));
The order scripts run in4 scripts
inline settingsinline

File ready at time 0

checkoutasync

File ready at time 2

appdefer

File ready at time 5

catalogmodule

File ready at time 1

inline settings -> checkout -> app -> catalog
Try it yourself

The scripts now run in this order: inline settings -> checkout -> app -> catalog. Only checkout, the async script, moves when its time changes. The deferred script (app) and the module (catalog) always run in their written order.

On a real network, you can't choose when a file finishes downloading. This model lets you choose, so you can see what changes. Reset puts the time back to 2.

In the playground, change the checkout download time from 2 to 9, then press Reset. At 9, checkout moves after the deferred script and the module. Those two stay in the same order. On a real network, download times change, so your app should never depend on the order of async scripts.

document.write writes into the HTML being read

document.write() adds HTML text straight into the HTML the parser is reading. Say an inline script calls it while the page is being read. Then the new HTML goes in at that exact spot. The parser reads it before the rest of the page.

Old pages used this to add ads or small pieces of HTML. But it breaks easily. The result depends on whether the browser is still reading the original page.

Adding HTML while the page is being readJavaScript
document.write("<p>Added while parsing</p>");
console.log("write was part of the current document stream");

This code only makes sense inside an inline script, while the page is being read. It isn't meant for the editor after the page has loaded. Line 1 adds a paragraph at the parser's current spot. Line 2 logs a note about that.

After the page has loaded, the parser is finished. Then document.write() starts a new, empty page that replaces the old one. In new code, add elements with DOM methods instead:

  • document.createElement("p") makes a new element.
  • textContent sets its text.
  • append puts it inside a parent element you choose.

These steps work the same at any time, whether or not the parser is still running.

Fix a slow page's script tags

Use a parser-blocking classic script only when the script really must run at its exact spot in the HTML. Most startup code should use modules or deferred scripts. Then the parser can keep building the page while the files download.

Picture a shop page. The checkout code needs the cart code to run first. The two plain classic tags below stop the parser twice. Each download must finish before the parser can go on.

Before: two scripts that stop the parserPop out in the code editor (opens in a new tab)HTML
<script src="/cart.js"></script>
<script src="/checkout.js"></script>

Line 1 stops the parser until cart.js has downloaded and run. Then line 2 stops it again for checkout.js. The order is correct, but the parser sits idle during each download.

After: cart and checkout in order, analytics on its ownPop out in the code editor (opens in a new tab)HTML
<script src="/cart.js" defer></script>
<script src="/checkout.js" defer></script>
<script src="/analytics.js" async></script>

Lines 1 and 2 now download while the parser keeps reading. They run after parsing, in order: cart, then checkout.

Line 3 loads analytics. Analytics code counts page visits, and no other script needs it. So async lets it run whenever its file is ready.

This change doesn't make a slow server faster. It removes waits the parser didn't need, and it shows clearly which script needs which. Measure the page after you change the tags. A tag only changes when a script runs. It doesn't make any file smaller.

Sort each script by when it runs
  • An inline classic script
  • A classic script with src and nothing else
  • A classic script with src and async
  • A module script with async
  • A classic script with src and defer
  • A module script without async
Try it yourself
0 of 6 correct

Put each card under the rule for when it runs, then read why.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mistakes about loading scripts

async changes when a script runs, not how fast it downloads. A file that downloads quickly runs sooner. But the word async doesn't make the file smaller or the server faster.

The preload scanner finds files to download. It doesn't run JavaScript. Deferred classic scripts keep their written order; async scripts don't. Module scripts without async wait for parsing and for every file they import.

  • "Async is always faster." It only changes when the script may run.
  • "Defer is async with a new name." Defer waits for parsing and keeps the written order.
  • "The preload scanner runs code." It only finds files whose addresses are written in the HTML.
  • "document.write adds elements like other DOM methods." It writes into the HTML being read, so its result depends on timing.

Each row of the table below is a word that is easy to mix up. Read it as: "this word means this, and it does not mean that."

Words that look alike but follow different rules
WordWhat it meansWhat it does not mean
asyncThe script runs as soon as its file is ready, maybe before parsing ends.A way to keep scripts in their written order.
deferThe script runs after parsing ends. Deferred classic scripts keep their written order.A way to run a script the moment its file arrives.
ModuleA module script waits like defer, unless it has async.A script whose timing changes when you add defer.
Preload scannerA helper that reads ahead in the HTML and starts downloading files early.A second parser that builds elements or runs JavaScript.

Practice exercises

Exercise 1 · Warm-upPredict what the script sees

The starter code stands for the moment a script runs. One paragraph exists already. The next one hasn't been read yet. The list before holds the paragraphs that exist. Type what the code prints, with no extra words.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const before = ["Hello"];
console.log(before.length);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the token kinds

    The tokenizer model reads <p>Hi</p> from left to right. Type the kinds of the first two tokens, separated by a comma.

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePredict the async order

      Two scripts use async. In this model, the later tag finishes downloading first. Type the order the model prints, separated by a comma. Notice that the written order doesn't decide which async script runs first.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const scripts = [
        { name: "first", kind: "async", downloadDoneAt: 3 },
        { name: "second", kind: "async", downloadDoneAt: 1 },
      ];
      console.log(executionOrder(scripts).join(","));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeKeep the deferred order

        The module finishes downloading before the deferred classic script. Both still wait until parsing ends. Type the order the model prints, separated by a comma. Use the order of the tags, not the download times.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const scripts = [
          { name: "app", kind: "defer", downloadDoneAt: 4 },
          { name: "catalog", kind: "module", downloadDoneAt: 1 },
        ];
        console.log(executionOrder(scripts).join(","));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose how to load analytics

          Your page sends a "page viewed" message with an analytics file. Neither the cart code nor the checkout code reads anything from it. Type one word for how its tag should load. It shouldn't wait for other scripts, and they shouldn't wait for it.

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFix the shop's startup

            A shop page loads the cart code first. Then the checkout code uses the cart code. Fix the tags so they don't stop the parser. Type one word for how both tags should load, then compare your answer with the shop example above.

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, keep three things apart: where the parser is, which files are ready, and the written order. The questions use the same small models as the lesson.

              Script loading quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Why does the first parser example print 1?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const scripts = [{ name: "a", kind: "async", downloadDoneAt: 4 }, { name: "b", kind: "async", downloadDoneAt: 1 }];
                console.log(executionOrder(scripts).join(","));

                Choose an answer to see the explanation.

              3. Question 3 of 7What does a preload scanner do?

                Choose an answer to see the explanation.

              4. Question 4 of 7Which statement describes defer?

                Choose an answer to see the explanation.

              5. Question 5 of 7How does a module script behave when it lacks async?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why is document.write() discouraged?

                Choose an answer to see the explanation.

              7. Question 7 of 7What does this deferred tag plan print first?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const plan = describeScriptTag({ src: "/checkout.js", defer: true });
                console.log(plan.executionGate);

                Choose an answer to see the explanation.

              Key takeaways

              • The tokenizer cuts HTML into tokens. The tree builder turns them into elements while the HTML is still arriving.
              • A classic script stops the parser at its spot in the HTML.
              • The preload scanner finds file addresses written in the HTML and starts downloading them early. It never runs code.
              • async runs a script when its file is ready. defer and modules without async wait for parsing.
              • Before you change a script tag, work out which scripts need which.
              • Avoid document.write in new code. Its result depends on whether the parser is still running.

              Remember the one-liner.
              A script tag decides when its code runs, not only what code runs.

              Coming next: The rendering pipeline.

              CompleteFrontend Clear concepts. Working examples.