Chapter 76: JavaScript in HTML

Native ES Modules with type="module"

Browser-native modularity, import/export semantics, automatic deferral, strict mode, and Import Maps.

LEARNING OBJECTIVES
  • Understand the 6 core architectural behaviors of <script type="module">.
  • Explain how ES modules enforce automatic deferral, strict mode, and lexical module scoping.
  • Use Top-Level await within browser-native module scripts.
  • Implement <script type="importmap"> to resolve bare module specifiers without build-step bundlers.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

In the early days of JavaScript, writing a complex web application was like having ten different authors scribbling on the exact same chalkboard at the same time. If Author A wrote var user = "Alice" and Author B wrote var user = "Bob", Author B erased Author A’s work. There were no boundaries; everything lived in one chaotic global room (window).

To fix this, developers invented complex bundling tools (Webpack, Browserify) to stitch thousands of files into giant monoliths before shipping them to the browser.

With Native ES Modules (type="module"), the browser became a modern library. Every module gets its own private, soundproof study room. What you declare in Room A stays in Room A unless you explicitly stamp it with export. Other rooms can cleanly import only the specific tools they need. The browser manages the dependency graph natively over HTTP/2 and HTTP/3 without requiring complex compilation toolchains.

Classic Global Script:
[ Script A: var total = 100 ] ──> [ window.total = 100 ] <── Overwritten!
[ Script B: var total = 200 ] ──> [ window.total = 200 ]

Native ES Module (<script type="module">):
[ Module A: const total = 100; export { total }; ] ──> Scoped to Module A
[ Module B: import { total } from './a.js';       ] ──> Clean, isolated reference (window.total is undefined)

Technical Deep Dive & Specifications

The 6 Core Behaviors of <script type="module">

When you add type="module" to a <script> tag, the browser switches from the classic script execution model to the ECMAScript Module (ESM) specification:

+---------------------------------------------------------------------------------------------------+
|                                  THE 6 PILLARS OF NATIVE ES MODULES                               |
+---------------------------------------------------------------------------------------------------+
|  1. DEFERRED BY DEFAULT:  Automatically behaves like `defer` (fetches in parallel, runs after DOM)|
|  2. STRICT MODE BY DEFAULT: `"use strict"` is permanently enabled; cannot be disabled.           |
|  3. TOP-LEVEL SCOPE:      Variables/functions are NOT attached to `window`.                       |
|  4. CORS ENFORCEMENT:     Cross-origin modules MUST serve valid CORS headers; file:// is blocked. |
|  5. TOP-LEVEL AWAIT:      You can `await` promises directly in module root without an async wrapper|
|  6. SINGLETON EXECUTION:  A module is fetched and executed ONCE, even if imported 50 times.       |
+---------------------------------------------------------------------------------------------------+

1. Automatic Deferral

You do not need to add the defer attribute to <script type="module">. The browser automatically fetches the module and its entire dependency graph in parallel in the background, executing them in document order after HTML parsing finishes and before DOMContentLoaded.

2. Top-Level await

In classic scripts, await was only valid inside an async function. In ES modules, top-level await is natively supported:

<script type="module">
  // Top-level await is 100% valid!
  const response = await fetch('/api/user');
  const user = await response.json();
  console.log('Logged in as:', user.name);
</script>

3. Module Singleton Execution

If moduleA.js and moduleB.js both contain import { db } from './database.js', the browser fetches database.js exactly once, executes it once, and shares the same live module export instance between both consumers.


Import Maps (<script type="importmap">)

Historically, browsers required relative or absolute URLs for imports:

// Valid in browsers:
import { format } from './utils/date.js';
import { Chart } from 'https://cdn.example.com/chart.js';

// INVALID in classic browser ESM (Bare Specifier):
import { format } from 'date-fns'; // ❌ TypeError: Failed to resolve module specifier

The Import Maps specification allows developers to declare alias mappings directly in HTML:

<script type="importmap">
{
  "imports": {
    "lodash": "https://cdn.jsdelivr.net/npm/[email protected]/lodash.js",
    "services/": "/src/services/",
    "@components/": "/src/ui/components/"
  }
}
</script>

<script type="module">
  // Now bare specifiers work natively in the browser!
  import { debounce } from 'lodash';
  import { AuthService } from 'services/auth.js';
</script>

⚠️ Strict Rule: An <script type="importmap"> element must appear before any <script type="module"> tags in the HTML document.


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL example.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

💻 Interactive Code Playground

Starter Code

Line-by-Line Code Breakdown

  • Lines 12–18 (<script type="importmap">): Maps the bare specifier "formatter" to an in-memory ESM module exporting formatCurrency().
  • Line 21 (<script type="module">): Starts an ES module script. Automatically runs with deferred timing and strict mode.
  • Line 22 (import { formatCurrency } from 'formatter'): Cleanly imports the function using the import map.
  • Lines 27–31 (await new Promise(...)): Demonstrates Top-Level await directly at the root level of the script.
  • Line 34 (const accountBalance): Scoped strictly to this module. Line 43 logs window.accountBalance is -> undefined, proving zero global namespace pollution.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
Banking Overview

[ Alex Mercer ]
Account Balance: $14250.75

(DevTools Console Output):
[Module Script]: Importing and executing...
[Module Script]: window.accountBalance is -> undefined

🏋️ Hands-On Exercise

🎯 The Challenge: Build a Zero-Build Modular Application with Import Maps

You are building an administrative dashboard without any bundler (no Webpack, Vite, or Babel). You want to use native browser ES modules to structure your code into clean, decoupled files.

Instructions:

  1. Define a <script type="importmap"> in <head> that maps:
    • "math-utils" to a module exporting a calculateDiscount(price, percent) function.
    • "dom-utils" to a module exporting a renderText(selector, text) function.
  2. Create a <script type="module"> in <head> that imports both utilities using their bare specifiers.
  3. Use top-level await to fetch a simulated product payload and render the discounted price into the DOM.
  4. Verify that no variables pollute the global window object.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Testing Modules via file:/// Protocol: Opening an HTML file with <script type="module"> directly in a browser via file:///C:/index.html will fail with a CORS error. ES modules must be served over http:// or https:// (e.g. using npx serve or Live Server).
  2. Placing <script type="importmap"> After Module Scripts: If an import map appears in the HTML after a module script that uses bare specifiers, the browser will throw an unrecoverable TypeError: Failed to resolve module specifier.
  3. Omitting File Extensions in Relative Imports: In Node.js/Webpack, you can write import { x } from './utils'. In native browser ESM (without an import map), you must include the full file extension: import { x } from './utils.js'.

💡 Pro Tips

  1. Preload Critical Module Subgraphs with <link rel="modulepreload">: For deep module dependency graphs (e.g., Module A imports B, which imports C), use <link rel="modulepreload" href="/src/c.js"> in <head>. This enables the browser to download and parse child modules in parallel before the parent finishes executing.
  2. Using async on Module Scripts: While module scripts are deferred by default, you can explicitly add <script type="module" async>. This causes the module to download its entire graph in parallel and execute immediately upon arrival (out-of-order), ideal for independent modern telemetry widgets.

📌 Key Takeaways

  • <script type="module"> enables native ECMAScript Modules (ESM) in modern browsers without build tools.
  • Module scripts are deferred by default, execute in strict mode, and possess private lexical scope (no window leakage).
  • Native ES modules fully support Top-Level await at the root of the file.
  • <script type="importmap"> maps bare package specifiers (e.g. "react") to CDN or local URLs.
  • Import maps must be declared in <head> before any module scripts.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Which of the following is TRUE regarding <script type="module">?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What happens if a <script type="importmap"> is placed AFTER a <script type="module"> that imports a bare module specifier?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

How many times does the browser execute a module if two different scripts both include import { config } from './config.js'?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP