Command Palette

Search for a command to run...

Hectal
PHASE 0Beginner ~19 min· topic 2 of 4

Topic 0.2

Adding JavaScript to a Page

In one line

JavaScript reaches a page through the <script> element, either written inline or loaded from a .js file; where you put it and whether you add defer, async or type="module" decides when it runs and what it can see.

Think of it like this

A play with a stage crew. The browser reads the HTML like a script for a play, building the stage line by line. A <script> is a stage-crew instruction: "move the red chair". If the instruction comes before the chair has been built, the crew can't find it. If it comes after, it works. defer is a note that says "do this once the whole stage is built".

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

script element
The <script> tag that puts JavaScript into a page.
Inline script
JavaScript written directly between <script> and </script> in the HTML.
External script
JavaScript kept in its own .js file and loaded with src.
DOM
Document Object Model: the tree of elements the browser builds from your HTML, which JavaScript can read and change.
Parsing
The browser reading the HTML from top to bottom and building the DOM.
Render-blocking
Something that makes the browser stop building or showing the page until it's done.
defer
Download now, run after the page is fully parsed, in order.
async
Download now, run the moment it arrives, in any order.
DOMContentLoaded
The event fired when the whole HTML has been parsed and the DOM is ready.

Step by step

01An inline script

The simplest way: write code inside a <script> element. It runs the moment the parser reaches it.

index.htmlwhole filehtml
<h1 id="title">Hello</h1>
<script>
  document.getElementById("title").textContent = "Hello from JavaScript";
</script>

02Move the code to its own file

Put the code in app.js and point to it with src. The path is relative to the HTML file, so app.js means "next to index.html" and js/app.js means "in the js folder".

app.jswhole filejavascript
document.getElementById("title").textContent = "Hello from app.js";

03Load it from the head with defer

This is the pattern to use by default. The download starts as soon as the browser reads the head, and the code runs once the body exists.

index.htmlwhole filehtml
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My page</title>
    <script src="app.js" defer></script>
  </head>
  <body>
    <h1 id="title">Hello</h1>
  </body>
</html>

04See how the three kinds load

A plain script stops the parser twice: once to download, once to run. defer downloads alongside parsing and waits. async downloads alongside parsing but jumps in as soon as it can.

See how the three kinds loaddiagram
Rendering diagram…

05Pick the right one

Your own page code: defer (or type="module"). Independent third-party code such as analytics: async. A tiny script that must run before the page shows (for example, setting a dark theme to avoid a flash): a small inline script in the head.

Try it yourself

  1. 1

    Fix the early script

    In the first example, move the head <script> to just before </body>. Predict what it logs now, then run it.

  2. 2

    Swap defer for async

    In the second example, change defer to async. A tiny data: file arrives almost at once, so watch where line 3 lands now. On a real network the position would change from load to load: that is why async scripts must not depend on each other.

  3. 3

    Remove type=module

    In the third example, delete type="module". Predict both lines before you run it. Why does the first one now print false?

Code & diagrams

A script only sees what is above it New tab

The head script ran before the parser had built the `<h1>`, so it found nothing (`null`). The second script came after it.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

In the head, the heading is: null
After it in the body: <h1#title>
The order of plain, inline and deferred scripts New tab

The examples can't load separate files, so the external scripts use `data:` URLs (the file's text written into the address). A real page writes `src="app.js"`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

1. plain external file in the head
2. inline script at the end of the body
3. deferred external file
4. DOMContentLoaded: the DOM is complete
Module scripts are deferred automatically ES2015+ New tab

The module was written first but ran last, after parsing, so it could see the paragraph.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Classic script runs first, during parsing
Module sees the paragraph below it: true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A plain script in the head touches the page

Load <script src="app.js"></script> in the <head>, where app.js runs document.getElementById("title").textContent = "Hi";.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot set properties of null (setting 'textContent')

Break #2

Self-close the script tag

Write <script src="app.js" /> instead of <script src="app.js"></script>.

terminal
$ the page shows
── what you'll see ──
Everything after the script tag is missing from the page.

Break #3

Wrong path to the file

Write <script src="js/app.js" defer></script> when the file is next to index.html.

terminal
$ DevTools → Console
── what you'll see ──
GET file:///C:/site/js/app.js net::ERR_FILE_NOT_FOUND

Myth vs fact

Myth

defer works on inline scripts too.

Fact

For classic scripts, defer and async only work with src. On an inline script they're ignored. (Inline module scripts are deferred anyway, and they do accept async.)

Myth

Scripts must always go at the end of the body.

Fact

That was the advice before defer. Today a deferred script in the head is better: it starts downloading sooner and still runs after parsing.

Myth

async is faster, so use it for everything.

Fact

async runs scripts in unpredictable order and possibly before the page is built. For scripts that depend on the page or on each other, use defer.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A plain script also waits for any stylesheets above it to load, because the code might ask for computed styles. That's another reason blocking scripts in the head slow down the first paint.

  • ▸

    Browsers have a preload scanner: while the main parser is blocked, a light scanner reads ahead and starts downloading the scripts, styles and images it finds. Blocking scripts are less costly than they once were, but still delay rendering.

  • ▸

    Inside an inline script, the text </script> ends the element even inside a string: "</script>" breaks the page. Write "<\/script>" instead. This is also why putting user data into inline scripts is a security risk (cross-site scripting); keep data in JSON and escape it.

  • ▸

    Modules are fetched with CORS (they must come from your site or a server that allows it) and don't run from file:// pages in most browsers. Use a local server such as npx serve when you start using modules (Topic on ES modules later in this course). <script nomodule> runs only in old browsers without module support.

Remember this

  1. 1

    The **<script> element** is how JavaScript gets into a page. Code written between <script> and </script> is an inline script. A script with a src attribute, like <script src="app.js"></script>, is an external script: the browser downloads that file and runs it. With src, anything written between the tags is ignored. You don't need type="text/javascript"; JavaScript is the default.

  2. 2

    External files are the normal choice for real sites. The same file can be used by many pages, the browser caches it (keeps a saved copy) so the next page loads faster, and your HTML stays clean. Inline scripts are fine for tiny bits of code and for examples.

  3. 3

    The browser builds the page by parsing HTML from top to bottom, turning tags into the DOM (Document Object Model: the tree of elements that JavaScript can read and change). A plain <script> blocks parsing: the browser stops, downloads the file if needed, runs it, and only then carries on. So a script can only see elements written above it. Ask for one below and you get null.

  4. 4

    defer (on external scripts) means: download the file in the background while parsing continues, then run it after the whole page is parsed, in the order the scripts appear, just before the DOMContentLoaded event (the moment the DOM is complete). This is the best default: <script src="app.js" defer></script> in the <head> starts the download early and never sees a half-built page.

  5. 5

    async (also external only) means: download in the background and run as soon as the file arrives, pausing parsing for that moment. Several async scripts run in whatever order they finish, and the page may not be complete yet. Use it for independent scripts like analytics that don't touch your page or other scripts.

  6. 6

    type="module" loads the file as an ES module (ES2015): it can use import and export, runs in strict mode, has its own scope, and is deferred automatically. Before defer was widespread, the old advice was "put scripts at the end of <body>"; that still works, but defer or modules in the head are the modern choice. Avoid onclick="..." attributes in HTML: they mix behaviour into content and are blocked by strict security policies.

Explain it without notes

01

What is the difference between a plain script, defer and async?

02

Why does document.getElementById return null when called from a plain script in the head?

03

Why are external script files preferred over inline scripts on real sites?

Practice

01

Write the head of a page that loads menu.js and then app.js so both run after parsing, with app.js guaranteed to run second.

02

Rewrite the first live example so the head script works without moving it, using DOMContentLoaded.

Trade-offs

  • ↔

    defer is predictable and doesn't block parsing, but the code runs a little later than a blocking script; for tiny setup code that must run before the first paint (like a theme switch), a small inline head script is worth the block.

  • ↔

    Many small script files are easy to organise but mean more requests; bundling them into one file means fewer requests but more to download when one line changes.

Done when you can

  • I can add JavaScript inline and from an external file.

  • I can explain why a plain script in the head can't see the body.

  • I can choose between plain, defer, async and type="module".

  • I always write the closing </script> tag.

  • I can find a failed script download in the Network tab.