cf.completefrontendCode editorOpen lab
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
    Trace parser workExplain how tokens become a tree and why an ordinary script can stop parser progress.
  • 02
    Choose a script modeCompare classic, async, defer, and module scripts by fetching, timing, and ordering.
  • 03
    Diagnose loading surprisesRecognize preload discovery, document.write hazards, and the difference between modeled and browser timing.

HTML arrives before JavaScript runs

HTML parsing turns incoming markup into document objects as bytes arrive. A script tag can run before the whole page exists.

Definition

Script loading is discovery, fetching, scheduling, execution, and then continued parsing.

A tag is a scheduling instruction. Its position, src, async, defer, and type="module" change its timeline.

Separate parser position, download readiness, and execution permission. Those are different facts, even when a waterfall makes them look close together.

This continues from What happens when you navigate.

The HTML tokenizer and tree builder

The tokenizer reads characters and emits tokens such as a start tag, text, and an end tag. The tree builder consumes those tokens and places nodes into the document tree.

This happens while HTML arrives. The browser does not wait for the final byte before it can create the first paragraph. That is why a later script can see an earlier element.

A tiny tokenizer and tree-builder modelPop 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 one small piece of markup. Line 2 turns it into tokens in source order. Line 3 gives those tokens to the tree builder. The first log prints start-tag,text,start-tag,text,end-tag,end-tag.

The second log prints "Hi ",b. Those are the paragraph's direct children: text first, then the bold element. The word there belongs inside that bold element, so it is not a direct paragraph child.

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

Replay a small tokenizer and tree-builder teaching model. It records real calls from this lesson, not browser-engine debugging.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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(","));
CallStoreChangeResultRun = next line. 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 recover from imperfect HTML. This small model treats a second <p> as the start of a new paragraph, even when the first paragraph did not include its closing tag.

A recovery rule in a teaching modelPop 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 deliberately omits the first closing paragraph tag. Line 2 finds the two recovered paragraphs. The logs print 2 and then "Tea","Cake". This is a teaching model, not browser source code: real tree building has many insertion modes and recovery rules.

Parser-blocking scripts

An inline classic script runs at its parser position. It can query nodes the tree builder has already made, but it cannot query nodes that remain later in the source.

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 first paragraph is parsed before the script. The script calls querySelectorAll while only that first paragraph exists, so it prints 1. Parsing resumes, then the browser creates the second paragraph.

An ordinary external classic script has the same position rule plus a network wait. The parser pauses while the file fetches, then pauses while that file executes. A stylesheet can delay execution too when the script might read computed styles.

Real-life analogyReading a recipe aloud

Read a recipe aloud. When you reach “go and buy milk”, everything stops until the milk arrives. A second friend can skim ahead and start buying other ingredients early.

With async, the milk is handed over the moment it arrives. With defer, it is used only after the whole recipe is read, in written order.

In real life: You reach go and buy milk
In JavaScript: Parser reaches a classic script
In real life: Reading stops until milk arrives
In JavaScript: Parsing waits for fetch and execution
In real life: A friend skims ahead for ingredients
In JavaScript: Preload scanner discovers later URLs
In real life: Milk used now or after reading
In JavaScript: async or defer scheduling

Where the analogy stops: A browser coordinates many requests; the recipe does not model every network detail.

The preload scanner

A blocked parser would otherwise hide later URLs. The preload scanner skims raw markup ahead and can start useful fetches before the main parser reaches those tags.

It is not a second tree builder and it never executes page JavaScript. It can discover a normal stylesheet, image, or script URL that is plainly written in the HTML response.

Keep a startup URL visiblePop out in the code editor (opens in a new tab)HTML
<script src="/checkout.js" defer></script>

The src value is visible in the response, so the browser can begin fetching /checkout.js early. The defer attribute still decides that execution waits until parsing is complete.

Discovery is different from creating a script laterPop 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 contains a visible src, so the first log prints found early. Line 2 only describes creating a script later; it has no URL in initial markup. The second log prints created later. The preload scanner cannot discover code before that code creates its URL.

async, defer, and module scripts

async fetches beside parsing and runs when ready. It fits independent work, but two async files do not promise document order. A later tag can run first when its download finishes first.

defer fetches beside parsing and runs after parsing. Deferred classic scripts keep source order, so they fit ordered startup files such as a cart file followed by a checkout file.

A module script fetches its module graph beside parsing. Without async, it has deferred-like timing; with async, it runs when its graph is ready. Imports make the graph matter, not only the file named in the HTML tag.

Four tags, four scheduling 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 through 6 make four tag strings: ordinary classic, async classic, deferred classic, and module. Line 8 prints 4. Line 9 states the key contrast: async runs when ready, while defer waits for parsing.

How each mode changes the timeline
ModeParsing and executionOrder driver
Classic externalStops parser while fetching and executingDocument position
async classicFetches beside parsing; runs when readyDownload completion
defer classicFetches beside parsing; runs after parsingDocument order
Module without asyncFetches module graph beside parsing; runs after parsingDocument order
Module with asyncFetches module graph beside parsing; runs when readyReadiness, not document order

Script execution order

Execution order is not one rule. It combines parser position, which downloads have become ready, and whether a script is allowed to run before parsing ends. Keeping those facts separate prevents most loading surprises.

Use one controlled model before trusting a live network trace. This model makes script kind and download completion explicit. It is a scheduling model, not a simulation of a browser engine.

A small execution-order modelPop 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 through 6 list scripts in document order. Line 3 is inline and runs in place. Line 4 is async and becomes ready at 2. Lines 5 and 6 wait for parsing even though the module reports readiness at 1.

Line 8 prints inline settings -> checkout -> app -> catalog. Inline runs in place, async runs by readiness, and defer or non-async module entries wait for parsing. The latter two stay in their written order in this model.

Step through the scheduling model
Step 0 of 6Ready
Your turn: follow the blue line

Replay a deterministic teaching model of script scheduling. Browser network timing is not deterministic, so the model makes its input explicit.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  { 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(" -> "));
CallStoreChangeResultRun = next line. 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 one async download time
Script order teaching modelJavaScript
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(" -> "));
Modeled execution order4 scripts
inline settingsinline

downloadDoneAt: 0

checkoutasync

downloadDoneAt: 2

appdefer

downloadDoneAt: 5

catalogmodule

downloadDoneAt: 1

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

The model now runs: inline settings -> checkout -> app -> catalog. The async entry moves with readiness; deferred and module entries still wait for parsing.

This controlled model exposes one input that real networks do not promise: when an async download becomes ready. Reset restores 2.

Change one download time, then reset it. Only the ready async position can change; the after-parsing group still appears after parsing. A real network can vary, which is exactly why app dependencies should not rely on async order.

document.write belongs to the parser era

document.write() writes markup into the active parser stream. During an inline parser script, it can add markup at that exact point, before the parser reads the following HTML.

That behavior explains old pages that load advertisements or small snippets this way. It also makes the API fragile: the result depends on whether the browser is still parsing the original document.

Writing while parsingPop out in the code editor (opens in a new tab)JavaScript
document.write("<p>Added while parsing</p>");
console.log("write was part of the current document stream");

Line 1 writes a paragraph into the current stream. Line 2 logs the narrow context that makes the write useful. In an inline parser script, the paragraph becomes part of the document at that position.

After load, calling it can open and replace the document instead. New code should make an element with createElement, set its content deliberately, and insert it with append. Those DOM operations do not depend on parser state.

Fix a slow page's script tags

Use a blocking classic script only when exact parser position is truly required. Most startup code should use modules or ordered deferred scripts, leaving the parser free to build the page while files download.

Imagine a shop page where checkout depends on cart setup. These two ordinary classic tags can stop parsing twice, and their source order forces each fetch into the parser's critical path.

Before: two parser-blocking startup scriptsPop out in the code editor (opens in a new tab)HTML
<script src="/cart.js"></script>
<script src="/checkout.js"></script>

Line 1 blocks parsing for cart.js. After it finishes, line 2 blocks parsing for checkout.js. The order is safe, but the parser is idle during each fetch.

After: ordered startup plus independent analyticsPop 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 fetch beside parsing and execute after parsing in order: cart before checkout. Line 3 is independent analytics, so async allows it to execute when ready without becoming an app dependency.

This does not make a slow server fast. It removes unnecessary parser waits and makes dependency rules clear. Measure the page after changing tags: a tag choice is a scheduling decision, not a magic speed switch.

Sort each script by when it can run
  • An inline classic script
  • A classic external script without attributes
  • An external classic script with async
  • A module script with async
  • An external classic script with defer
  • A module script without async
Try it yourself
0 of 6 correct

Place each card by its timing rule, then read the explanation.

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

Common loading misconceptions

Async changes scheduling, not network speed. A faster download can make an async script execute earlier, but the attribute does not shrink the file or improve the server response.

The preload scanner discovers resources, not JavaScript behavior. Deferred classics retain document order; async files do not. Non-async module scripts wait for parsing and for the module graph they need.

  • “Async is always faster.” It only changes when execution may happen.
  • “Defer is async with a new name.” Defer waits for parsing and preserves order.
  • “A preload scanner runs code.” It only finds resources that HTML already reveals.
  • “document.write updates the DOM.” It writes the parser stream and depends on timing.
Similar words, different rules
TermWhat it meansNot this
asyncRuns when its script is ready, possibly before parsing ends.A way to preserve script order.
deferRuns after parsing and keeps document order for deferred classics.A way to run immediately after download.
ModuleActs like deferred loading unless it has async.A guarantee that defer changes its timing.
Preload scannerSpeculatively discovers fetchable resources in markup.A second DOM tree builder or JavaScript executor.

Practice exercises

Exercise 1 · Warm-upPredict parser visibility

A script has reached a point where one paragraph already exists and a later paragraph does not. Read the starter code as that moment in parsing. Type the output, without adding a sentence or punctuation.

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 token kinds

    A tiny tokenizer reads <p>Hi</p> from left to right. Give the first two token kinds, separated by a comma. Focus on what the characters mean before any tree exists.

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

      Exercise 3 · PracticePredict async order

      Two independent scripts use async. The later one finishes downloading first in this controlled model. Predict the comma-separated output and notice why written order is not a dependency contract.

      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 deferred order

        The module finishes fetching before the deferred classic file. Both still wait until parsing finishes. Type the resulting comma-separated order, using the order of the tags rather than 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 analytics loading

          Your page sends a page-view event, but neither cart nor checkout reads anything from that analytics file. Choose one loading mode for its tag. The goal is to avoid creating an order dependency where none exists.

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

            Exercise 6 · ChallengeFix app startup

            A real shop page loads cart setup first, then checkout startup reads the cart API. Fix those tags without bringing back parser blocking. Name a suitable mode, then compare your answer with the practical example.

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

              Check your understanding

              Separate parser position, readiness, and document order before choosing an answer. The questions use the same small models as the article, so every output has one explicit reason.

              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

              • Tokens become a document tree while HTML arrives.
              • Classic scripts can stop parsing at their source position.
              • The preload scanner can discover visible URLs early, but cannot run code.
              • Async runs when ready; defer and non-async modules wait for parsing.
              • Use a clear dependency rule before changing script tags.
              • Avoid document.write in new work because parser state makes it fragile.

              Remember the one-liner.
              A script tag is a scheduling instruction as well as code.

              Coming next: The rendering pipeline.

              CompleteFrontend Clear concepts. Working examples.