Adding JavaScript to HTML
Learn the different ways to include JavaScript in an HTML page and understand the practical differences between inline, internal, and external scripts, including defer, async, and modules.
JavaScript does nothing on its own until it is connected to an HTML page, and there are several ways to do that connection, each with different trade-offs for organization and page-load performance.
Inline scripts
An inline script is JavaScript written directly inside an HTML attribute, such as an onclick handler: <button onclick="alert('Hi!')">Click</button>. It mixes JavaScript logic directly into HTML markup.
This approach is almost always discouraged in real projects: it tightly couples structure and behavior, makes code hard to read and reuse, cannot easily be cached separately by the browser, and is restricted by modern security policies (like Content Security Policy) on many sites. It is worth recognizing when you see it, but you should avoid writing it yourself.
Internal scripts
An internal script places JavaScript inside a <script> tag directly within the HTML document, rather than in an attribute: <script>console.log("Hello");</script>. This keeps the code out of individual element attributes, but it is still embedded inside the HTML file itself rather than kept separate.
Internal scripts are useful for very small, page-specific snippets or quick demos, but for anything beyond a few lines, they suffer from the same core problem as inline scripts: the JavaScript cannot be reused across multiple pages or cached independently by the browser.
External scripts
An external script lives in its own separate .js file and is connected to the HTML page using the src attribute of a <script> tag: <script src="app.js"></script>. This is the standard, recommended approach for any real project.
External scripts can be reused across multiple HTML pages, cached by the browser (so returning visitors don't re-download the same file), edited independently by JavaScript-focused tools, and kept cleanly separate from HTML markup — following the general best practice of separating structure (HTML), presentation (CSS), and behavior (JavaScript) into their own files.
Controlling load order: defer and async
By default, when the browser's HTML parser reaches a <script> tag, it stops parsing the HTML entirely, downloads the script (if external), executes it, and only then resumes parsing the rest of the page. For a large or slow script, this can noticeably delay the page from appearing.
The defer attribute tells the browser to download the script in the background while continuing to parse the HTML, and only execute it after the HTML document has been fully parsed — and multiple deferred scripts still run in their original order. This is the recommended default for most page scripts, since it avoids blocking rendering while guaranteeing the DOM is ready and script order is preserved.
The async attribute also downloads the script in the background without blocking parsing, but executes it as soon as it finishes downloading — whenever that happens — without waiting for HTML parsing to finish, and without guaranteeing order relative to other async scripts. This suits independent scripts (like analytics) that don't need the DOM or a particular order.
JavaScript modules
A module is a JavaScript file that can explicitly export values (functions, variables, objects) for other files to import, using the export and import keywords. Modules are included in HTML with <script type="module" src="app.js"></script>.
Module scripts behave like defer automatically (they don't block parsing and run after the document is parsed), and they run in their own isolated scope by default, meaning variables declared at the top level of a module do not leak into the global scope the way they do in a regular script. Modules are the modern standard for structuring non-trivial JavaScript applications into separate, reusable files.
Ways to include JavaScript
defer and async only affect external scripts (a src attribute) — they are ignored on inline/internal scripts. Placing a regular (non-deferred, non-async) <script> at the very end of <body> is a common older technique to avoid blocking rendering, but defer is generally the cleaner modern approach. A type="module" script always behaves as deferred, regardless of whether you add the defer attribute.
<!-- Internal -->
<script>
console.log("Runs inline in the HTML file");
</script>
<!-- External, default (blocking) -->
<script src="app.js"></script>
<!-- External, deferred -->
<script src="app.js" defer></script>
<!-- External, async -->
<script src="app.js" async></script>
<!-- Module -->
<script type="module" src="app.js"></script>Code Examples
This is the recommended everyday pattern: link an external script with defer, commonly placed in the <head> or just before </body>. Expected result: A page showing "Welcome" that renders immediately while app.js downloads in the background and runs afterward.
<script src="app.js" defer></script> — downloads app.js without blocking HTML parsing, then runs it once the document is ready.
This shows two files connected as ES modules: math.js exports a function, and app.js imports and uses it. In HTML, app.js would be loaded with <script type="module" src="app.js"></script>. Expected result: 5 import { add } from "./math.js"; pulls a specific named export from another module file.
Common mistake: Forgetting type="module" on the <script> tag is a common mistake — without it, the browser treats the file as a regular script and import/export syntax throws a SyntaxError.
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Welcome</h1>
<script src="app.js" defer></script>
</body>
</html>// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from "./math.js";
console.log(add(2, 3));defer vs async vs default timing
defer waits until parsing is complete and preserves order; async runs the moment the file is ready, whenever that is.
DEFAULT <script src>:
Parse HTML --[STOP]-- download+run script --[RESUME]-- parse rest
defer:
Parse HTML (download in background) ------------> finish parsing -> run script
async:
Parse HTML (download in background) -> [run as soon as ready, may interrupt parsing]Important Rules
External scripts (separate .js files linked with src) are the standard approach for real projects — prefer them over inline or internal scripts.
Use defer for scripts that need the DOM to exist or must run in a specific order relative to other scripts.
Use async for independent scripts that don't touch the DOM and don't depend on load order, like analytics.
type="module" scripts behave like defer automatically and have their own isolated top-level scope.
Where this shows up
Nearly every production website loads its JavaScript as one or more external, deferred files (or as bundled ES modules produced by build tools like Vite or Webpack) rather than writing logic directly inside HTML — this keeps pages fast to render and code easy to maintain and cache.
<script src="app.js"></script> placed at the very top of <head> with no defer/async on a script that touches the DOM.
<script src="app.js" defer></script>, or placing the script just before </body>.
A blocking script at the top of <head> runs before the <body> exists yet, so any code trying to select DOM elements will fail to find them; defer (or moving the script to the end of the body) ensures the DOM is ready first.
[Multiple Choice] Which script-loading attribute guarantees scripts run in their original order, only after the HTML has finished parsing? A. async B. defer C. type="text/javascript" D. None of the above guarantee order
B. defer — defer downloads scripts in the background without blocking parsing, and executes them in their original document order after parsing completes; async makes no such order guarantee.
[True / False] A <script type="module"> tag behaves like a regular blocking script by default. A. True B. False
B. False — False. Module scripts are deferred automatically — they download without blocking HTML parsing and execute after the document is parsed, just like an explicit defer attribute.
Write the HTML <script> tag needed to load an external file named site.js so that it does not block page rendering and runs only after the HTML has finished parsing.
<script src="site.js" defer></script> You need the src attribute plus one specific boolean attribute covered in this lesson.