Skip to content
JavaScript js getting-started 4 min read

JavaScript in HTML

JavaScript runs in the browser only after it has been delivered to the page, and the way you load it has a direct effect on how fast a page becomes interactive. The <script> tag is the single mechanism for adding code to HTML, but where you put it and which attributes you set control whether the browser stalls rendering, downloads in parallel, or runs your code in the right order. Getting this right is one of the cheapest performance wins available, so it is worth understanding precisely.

The script tag

There are two ways to attach JavaScript to a page: inline code written directly between the tags, and external code referenced with the src attribute.

<!-- Inline script -->
<script>
  console.log("Hello from inline JS");
</script>

<!-- External script -->
<script src="/js/app.js"></script>

External files are almost always the better choice for real applications. They can be cached by the browser, shared across pages, minified by your build tool, and kept out of your HTML. Inline scripts are fine for tiny bootstrapping snippets or analytics tags, but they cannot be cached and they bloat the HTML document.

A <script> element with a src attribute must be empty — any code written between the tags is ignored. Use one or the other, never both.

Where to place the script

By default, when the HTML parser reaches a plain <script> tag it stops parsing the document, downloads the script (if external), executes it, and only then resumes building the page. This blocking behaviour is why placement matters so much.

Two classic placements exist:

<!-- In the head: runs before the page is built (blocks rendering) -->
<head>
  <script src="/js/app.js"></script>
</head>

<!-- At the end of body: DOM is ready, nothing to block -->
<body>
  <!-- page content -->
  <script src="/js/app.js"></script>
</body>

Placing scripts at the end of <body> was the standard fix for years: by the time the browser reaches the tag, the DOM elements your code touches already exist, and the script no longer blocks the visible content from rendering. The modern answer, however, is to keep scripts in <head> and add a loading attribute instead.

async and defer

Both async and defer apply only to external scripts (they require src). They tell the browser to download the file in parallel with HTML parsing, so the download never blocks rendering. The difference is when the code executes.

  • defer — download in parallel, then execute after the HTML is fully parsed, just before the DOMContentLoaded event. Deferred scripts run in document order.
  • async — download in parallel, then execute as soon as the download finishes, pausing parsing at that moment. Order is not guaranteed; whichever arrives first runs first.
<head>
  <script src="/js/app.js" defer></script>
  <script src="/js/analytics.js" async></script>
</head>

How they compare

BehaviourPlain <script>asyncdefer
Blocks HTML parsing during downloadYesNoNo
Blocks HTML parsing during executionYesYes (when ready)No
Execution timingImmediately, inlineAs soon as downloadedAfter parsing, before DOMContentLoaded
Preserves document orderYesNoYes
Works on inline scriptsn/aNoNo
DOM guaranteed availableOnly if placed lastNoYes

Load and execute timing

The diagram below contrasts the three modes. is HTML parsing, is script download, and is script execution.

plain:   ═══■──────────█████═══════════════
              (parsing stops to fetch + run)

async:   ═══▓▓▓▓════█████═══════════════
              (download parallel, runs ASAP, pauses parse)

defer:   ═══▓▓▓▓══════════════════█████
              (download parallel, runs after parse completes)

With defer, parsing finishes uninterrupted and your code runs against a complete DOM. With async, the script can interrupt parsing the instant it lands — great for independent scripts, risky for anything that depends on the DOM or on another file.

Module scripts

Setting type="module" opts a script into ES modules, which enables import/export, runs the code in strict mode, and scopes top-level variables to the module instead of the global object. Crucially, module scripts are deferred by default — there is no need to add defer.

<script type="module" src="/js/main.js"></script>
// main.js
import { greet } from "./greet.js";

document.querySelector("#app").textContent = greet("DevCraftly");
console.log("Module loaded after the DOM was parsed");

Output:

Module loaded after the DOM was parsed

Use type="module" for any modern app: you get clean imports, deferred execution, and strict mode for free. For a one-off script that must run before everything else (rare), reach for async.

Best practices

  • Prefer external files over inline scripts so the browser can cache and your build tools can minify.
  • Default to <script type="module"> for application code — it defers automatically and unlocks import/export.
  • Use defer for non-module scripts that touch the DOM or depend on each other; it keeps execution in order.
  • Reserve async for fully independent scripts such as analytics or ad tags, where order does not matter.
  • Keep scripts in <head> with defer/module rather than at the end of <body>; the browser starts downloading sooner.
  • Never write code between a <script> tag’s open and close when src is present — it will be silently ignored.
Last updated June 1, 2026
Was this helpful?