Skip to main content
Knowledge Hub

Script Type Module

How ES modules load and execute in the browser

Last updated: March 5, 2025

You can also load scripts using the ES Module system. It is similar to defer but provides additional features. type=“module” equals defer plus ES Modules plus strict mode plus dependency management.

<script type="module" src="main.js"></script>

Understanding how modules load helps you organize code effectively and leverage modern JavaScript features.

Behavior

type=“module” scripts automatically behave like defer. But they also:

  • Maintain execution order based on import dependencies
  • Run in strict mode and module scope (variables aren’t global)
  • Support import and export (modular JavaScript support)

Example:

// main.js
import { greet } from './utils.js';
greet('John');

// utils.js
export function greet(name) {
  console.log(`Hello, ${name}!`);
}

How the Browser Loads Module Dependencies

When you write:

<script type="module" src="main.js"></script>

The browser loads and executes your scripts through the ES Module system, automatically handling dependencies.

Step-by-Step

  1. HTML Parsing Begins: The browser starts reading HTML and encounters <script type="module" src="main.js">.

  2. Module Scheduled:

    • Fetches main.js asynchronously (same as defer)
    • HTML parsing continues (same as defer)
  3. Imports Detected: The module parser scans main.js before execution. For every line like:

import { greet } from './utils.js';

The browser automatically fetches all required modules and their dependencies. If the module parser found one or more import statements, it began fetching those files (and their dependencies) before running any code.

Execution waits until all are fetched and parsed. Only when every dependency is ready, the browser executes them in the correct order.

  1. Dependency Graph Built:
main.js
  └── utils.js
        └── constants.js

Each module is downloaded once, even if imported multiple times (extra behavior).

  1. Execution Order: Runs bottom-up in dependency order (extra, determined by imports):

constants.js → utils.js → main.js

  1. Caching: Modules are cached and reused (extra, efficient re-imports).

Visualization

HTML Parser

<script type="module" src="main.js">

main.js ──→ utils.js ──→ constants.js
   ↓                 ↓
executes last   executes first

Quick Comparison

AttributeHTML ParsingExecution TimingScopeSupports Imports?Use Case
deferNon-blockingAfter DOM parsedGlobal scopeNoDOM manipulation
type=“module”Non-blocking (same as defer)After DOM parsed (same as defer)Module scope (extra)Yes (extra)Modern modular scripts

Key Differences from defer

Module Scope

Variables declared in modules are not global:

// With defer (or no attribute)
var x = 10;
console.log(window.x); // 10 (global)

// With type="module"
var x = 10;
console.log(window.x); // undefined (module scope)

Strict Mode

Modules automatically run in strict mode:

// With type="module"
y = 20; // Error: y is not defined (strict mode)

Import and Export

Modules support import and export syntax:

// utils.js
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;

// main.js
import { add, PI } from './utils.js';
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

CORS Requirement

Module scripts require CORS headers when loaded from different origins. Regular scripts don’t have this requirement.

<!-- This will fail without CORS headers -->
<script type="module" src="https://example.com/module.js"></script>

Module Loading Process

  1. Parse: Browser parses the module and identifies all imports
  2. Fetch: Downloads all dependencies in parallel
  3. Instantiate: Creates module instances and binds imports/exports
  4. Evaluate: Executes module code in dependency order

Dynamic Imports

Modules also support dynamic imports for code splitting:

button.addEventListener('click', async () => {
  const module = await import('./heavy-feature.js');
  module.initialize();
});

This loads modules only when needed, improving initial page load performance.

Benefits of type=“module”

  1. Encapsulation: Module scope prevents global namespace pollution
  2. Dependency Management: Automatic handling of dependencies
  3. Code Organization: Natural code splitting and organization
  4. Modern Syntax: Full ES6+ syntax support
  5. Tree Shaking: Bundlers can eliminate unused code
  6. Strict Mode: Safer code by default

Fallback for Older Browsers

You can provide a fallback for browsers that don’t support modules:

<script type="module" src="main.js"></script>
<script nomodule src="main-legacy.js"></script>

Modern browsers load the module script and ignore nomodule. Older browsers ignore type=“module” and load the nomodule script.

Summary

type=“module” provides all the benefits of defer (non-blocking download, execution after DOM parsing) plus modern JavaScript module features (import/export, module scope, automatic strict mode).

For modern web applications, type=“module” is the recommended way to load JavaScript. It promotes better code organization, prevents global namespace pollution, and enables modern development practices like code splitting and tree shaking.

Understanding how modules load and execute is essential for building scalable, maintainable web applications.