Linking Scripts with the Script Element
The <script> element embeds or references JavaScript in an HTML document. How and where you use it has a direct effect on page load performance, because by default, scripts block HTML parsing.
External vs inline scripts
<!-- External: src points to a separate .js file -->
<script src="/js/app.js"></script>
<!-- Inline: JavaScript written directly between the tags -->
<script>
console.log('Hello from an inline script');
</script>src (external) lets browsers cache and reuse the file across pages, and keeps HTML and JS separated.
Inline scripts avoid an extra network request, which suits small, page-specific snippets.
You cannot use both src and inline content in the same tag — src wins and the inline content is ignored.
Execution timing: default, async, and defer
By default, when the parser hits a script tag, it stops parsing HTML, fetches the script (if external), runs it immediately, and only then resumes parsing. The async and defer attributes change this.
<script src="/js/blocking.js"></script> <script async src="/js/analytics.js"></script> <script defer src="/js/app.js"></script>
Think of it as a timeline. With no attribute, HTML parsing pauses, the script downloads, then runs, then parsing resumes — a full stop. With async, the script downloads in parallel with parsing but runs the instant it finishes, interrupting parsing wherever it happens to be. With defer, the script also downloads in parallel, but execution is held until parsing is completely finished, and deferred scripts run in their original document order.
Attribute | Download | Execution | Blocks parsing? |
|---|---|---|---|
(none) | Blocking | Immediately, in place | Yes |
async | Parallel | As soon as ready (any order) | Briefly, on completion |
defer | Parallel | After parsing completes, in order | No |
Use defer for scripts that need the full DOM and must run in a predictable order (most application code).
Use async for independent scripts with no dependencies on the DOM or on each other, like analytics or ads.
Avoid the no-attribute default for anything non-trivial — it is the slowest option for the page.
type="module"
<script type="module" src="/js/app.mjs"></script>
Enables native ES module import/export syntax in the script.
Module scripts are deferred by default — they behave like defer even without the attribute.
Executed in strict mode automatically, and each module has its own top-level scope.
Placement: head vs end of body
Before async/defer existed, the standard advice was to place scripts at the very end of body so they would not block rendering. That advice still works, but async/defer now give you finer control from the head.
Placement | Tradeoff |
|---|---|
End of body (no attribute) | Simple and safe, but the browser cannot start downloading the script until it has parsed the entire page |
head with defer | Download starts immediately (better), execution still waits for parsing to finish (safe) |
head with async | Download starts immediately, executes as soon as ready — best for independent, order-agnostic scripts |
defer scripts in <head> is generally the best default for application code today: downloads start as early as possible, without risking a script running before the DOM it depends on exists.defer and async have no effect on inline scripts without a src attribute — they only change behavior for externally loaded scripts.