JavaScript Where To
JavaScript lives inside your HTML — but where you put it and how you load it changes when it runs and how fast your page feels. Get this wrong and your page is blank for seconds longer than it needs to be. Get it right and the page loads instantly while scripts hum along in the background.
The <script> Tag
Every line of JavaScript on a web page is loaded via the <script> tag. There are exactly two flavours:
<!-- 1. Inline — code lives directly inside the tag -->
<script>
console.log("Hello from inline JS");
</script>
<!-- 2. External — code lives in a separate .js file -->
<script src="app.js"></script>
The external form is what real projects use. It lets you cache the file, share it across pages, and keep your HTML readable.
Where Can You Put the <script> Tag?
Anywhere inside <head> or <body>. The placement controls when the browser pauses parsing your HTML to download and execute the script.
<!DOCTYPE html>
<html>
<head>
<!-- Option A: in <head> -->
<script src="app.js"></script>
</head>
<body>
<h1>My Page</h1>
<!-- Option B: at the end of <body> -->
<script src="app.js"></script>
</body>
</html>
Why Placement Matters
The browser parses HTML top to bottom. When it hits a classic <script> tag, it stops, downloads the file, executes it, and then keeps parsing. That blocking behaviour is what causes the dreaded blank-page-for-2-seconds effect.
HTML parsing ─────┐ ┌──────────── continues
│ │
▼ │
<script> │
download │ ← parsing PAUSED here
execute │
│ │
└───────┘
| Placement | Blocks Rendering? | Sees the DOM? | Use When… |
|---|---|---|---|
<head> (plain) |
❌ Yes — bad | ❌ No, DOM not built yet | Almost never |
Before </body> |
✅ No — DOM is ready | ✅ Yes | Old-school, still works |
<head> + defer |
✅ No — runs after DOM ready | ✅ Yes | Best default |
<head> + async |
Partly — runs ASAP | ⚠️ Maybe | Independent scripts (analytics) |
<script type="module"> |
✅ Deferred by default | ✅ Yes | Modern ESM apps |
Inline vs External Scripts
<!-- ❌ Avoid: inline -->
<script>
document.querySelector("h1").style.color = "red";
</script>
<!-- ✅ Prefer: external -->
<script src="/assets/js/app.js" defer></script>
| Inline | External | |
|---|---|---|
| Cached by browser | ❌ No — re-downloads on every page | ✅ Yes — one cache, many pages |
| Easy to reuse across pages | ❌ Copy-paste | ✅ Just reference the same file |
| Friendly to source maps / linters | ❌ Limited | ✅ Full tooling |
| Bypasses strict CSP | ❌ No (needs 'unsafe-inline') |
✅ Yes |
| Quick one-liner | ✅ Yes | Overkill |
Tip: Use inline only for tiny bootstraps (e.g. a global window.APP_CONFIG = {...} injected by the server). Everything else goes in an external file.
The defer Attribute
defer tells the browser: “download this script in parallel with HTML parsing, but don’t execute it until the HTML is fully parsed.”
<head>
<script src="app.js" defer></script>
<script src="vendor.js" defer></script>
</head>
This gives you the best of both worlds:
- Download starts immediately (no waiting)
- Execution waits for the DOM
- Multiple deferred scripts execute in document order
HTML parsing ────────────────────────────────────── done ─▶ run deferred scripts in order
▲
│ scripts download in parallel
app.js, vendor.js
The async Attribute
async also downloads in parallel — but executes the script as soon as it’s ready, even if HTML isn’t done parsing. Execution order between async scripts is not guaranteed.
<head>
<script src="analytics.js" async></script>
<script src="adsense.js" async></script>
</head>
Use async for scripts that are independent: analytics, ads, error reporters. Don’t use async if a script depends on another script or on the DOM.
defer vs async Side-by-Side
Plain ────HTML────[stop]download─exec[resume]────HTML──── ← bad
defer ────HTML───────────────────────────────────HTML────▶ exec (DOM ready)
▲download
async ────HTML────────────────[exec]────HTML─────HTML──── ← anywhere
▲download
defer |
async |
|
|---|---|---|
| Downloads in parallel | ✅ | ✅ |
| Blocks HTML parsing | ❌ | ❌ |
| Execution order preserved | ✅ Yes | ❌ Random |
| Runs after DOM ready | ✅ Yes | ❌ As soon as downloaded |
| Best for | App code | Analytics, ads, telemetry |
Modules — type="module"
ES modules (covered in chapter 46) are deferred by default. They also enable import/export:
<script type="module" src="main.js"></script>
// main.js
import { greet } from "./utils.js";
greet("World");
You don’t need defer on modules — it’s automatic.
A Realistic Modern Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App</title>
<!-- Analytics: independent, fire-and-forget -->
<script src="https://analytics.example.com/tag.js" async></script>
<!-- App code: deferred, runs after DOM is ready -->
<script src="/assets/js/app.js" defer></script>
</head>
<body>
<h1>Welcome</h1>
<!-- No <script> tags at the bottom needed -->
</body>
</html>
Common Mistakes
<!-- ❌ Blocking the page for a script that needs the DOM -->
<head>
<script src="app.js"></script> <!-- runs before <body> exists -->
</head>
<!-- ✅ Fix: add defer -->
<head>
<script src="app.js" defer></script>
</head>
<!-- ❌ Using async for dependent scripts -->
<script src="jquery.js" async></script>
<script src="my-plugin.js" async></script> <!-- might run before jQuery! -->
<!-- ✅ Fix: use defer to preserve order -->
<script src="jquery.js" defer></script>
<script src="my-plugin.js" defer></script>
Warning: Never mix defer and async on the same tag. If both are present, async wins — defer is silently ignored.
Quick Decision Guide
Need it to run? ───── Is it independent (analytics, ads)?
│
┌──── Yes ──┴── No ────┐
▼ ▼
async Does it use the DOM?
│
┌──── Yes ──┴── No ────┐
▼ ▼
defer defer
(always safe) (still safe)
Rule of thumb: Default to defer in <head> for all your app code. Reach for async only when a script truly doesn’t care about your code or the DOM.
What’s Next?
Now that your scripts are loading correctly, let’s actually print something. We’ll look at the four ways JavaScript can output values — to the page, to the console, to alert dialogs, and into HTML directly.