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
-
HTML Parsing Begins: The browser starts reading HTML and encounters
<script type="module" src="main.js">. -
Module Scheduled:
- Fetches main.js asynchronously (same as defer)
- HTML parsing continues (same as defer)
-
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.
- Dependency Graph Built:
main.js
└── utils.js
└── constants.js
Each module is downloaded once, even if imported multiple times (extra behavior).
- Execution Order: Runs bottom-up in dependency order (extra, determined by imports):
constants.js → utils.js → main.js
- 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
| Attribute | HTML Parsing | Execution Timing | Scope | Supports Imports? | Use Case |
|---|---|---|---|---|---|
| defer | Non-blocking | After DOM parsed | Global scope | No | DOM 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
- Parse: Browser parses the module and identifies all imports
- Fetch: Downloads all dependencies in parallel
- Instantiate: Creates module instances and binds imports/exports
- 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”
- Encapsulation: Module scope prevents global namespace pollution
- Dependency Management: Automatic handling of dependencies
- Code Organization: Natural code splitting and organization
- Modern Syntax: Full ES6+ syntax support
- Tree Shaking: Bundlers can eliminate unused code
- 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.