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
.jsfile and loaded withsrc. - 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.
<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".
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.
<!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.
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
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
Swap defer for async
In the second example, change
defertoasync. A tinydata: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 whyasyncscripts must not depend on each other. - 3
Remove type=module
In the third example, delete
type="module". Predict both lines before you run it. Why does the first one now printfalse?
Code & diagrams
The head script ran before the parser had built the `<h1>`, so it found nothing (`null`). The second script came after it.
Preview
Console
Nothing logged yet.Expected console output
In the head, the heading is: null
After it in the body: <h1#title>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
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 completeThe module was written first but ran last, after parsing, so it could see the paragraph.
Preview
Console
Nothing logged yet.Expected console output
Classic script runs first, during parsing
Module sees the paragraph below it: trueBreak 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";.
Break #2
Self-close the script tag
Write <script src="app.js" /> instead of <script src="app.js"></script>.
Break #3
Wrong path to the file
Write <script src="js/app.js" defer></script> when the file is next to index.html.
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 asnpx servewhen 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
The **
<script>element** is how JavaScript gets into a page. Code written between<script>and</script>is an inline script. A script with asrcattribute, like<script src="app.js"></script>, is an external script: the browser downloads that file and runs it. Withsrc, anything written between the tags is ignored. You don't needtype="text/javascript"; JavaScript is the default. - 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
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 getnull. - 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 theDOMContentLoadedevent (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
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
type="module"loads the file as an ES module (ES2015): it can useimportandexport, runs in strict mode, has its own scope, and is deferred automatically. Beforedeferwas widespread, the old advice was "put scripts at the end of<body>"; that still works, butdeferor modules in the head are the modern choice. Avoidonclick="..."attributes in HTML: they mix behaviour into content and are blocked by strict security policies.
Explain it without notes
What is the difference between a plain script, defer and async?
Why does document.getElementById return null when called from a plain script in the head?
Why are external script files preferred over inline scripts on real sites?
Practice
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.
Rewrite the first live example so the head script works without moving it, using DOMContentLoaded.
Trade-offs
- ↔
deferis 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,asyncandtype="module".I always write the closing
</script>tag.I can find a failed script download in the Network tab.