How type=“module” Applies Defer in JavaScript
When you include a script using the type="module"
attribute, modern browsers automatically treat that script as deferred
by default. This article explains how the HTML specification implements
this behavior, why JavaScript modules require non-blocking loading, and
how module execution integrates with the Document Object Model (DOM)
parsing cycle.
The Default Behavior of Standard vs. Module Scripts
By default, standard JavaScript scripts
(<script src="...">) are parser-blocking. When the
HTML parser encounters a classic script tag, it pauses parsing the HTML
document, downloads the script file, executes it immediately, and only
then resumes building the DOM.
Adding type="module" to a <script>
tag fundamentally alters this loading lifecycle. The browser
automatically applies the defer semantic without requiring
the explicit defer attribute. The browser initiates the
download of the module file in parallel with HTML parsing and waits to
execute the code until the entire HTML document has been fully
parsed.
Why Modules Are Deferred by Design
JavaScript modules rely on the ECMAScript module (ESM) system, which
allows files to import dependencies using import
statements.
- Dependency Resolution: A single module file often imports other modules, which may in turn import further dependencies. The browser must fetch, parse, and instantiate this entire dependency graph before any code can run.
- Performance Protection: If module scripts blocked the HTML parser synchronously while resolving their entire dependency tree over the network, page rendering performance would degrade significantly.
- DOM Availability: Modules frequently interact with
the fully constructed DOM. Deferring execution guarantees that elements
are available in the document without requiring wrappers like
DOMContentLoadedevent listeners.
Execution Order and the DOM Lifecycle
Module scripts behave identically to classic scripts that have the
defer attribute enabled:
- Non-Blocking Fetching: External module files and
all nested static
importdeclarations are downloaded in parallel in the background. - Order Preservation: Even if smaller dependency files finish downloading first, multiple deferred module scripts execute in the exact order they appear in the HTML source document.
- Timing: Execution occurs after the document has
finished parsing, immediately before the browser fires the
DOMContentLoadedevent.
Overriding Defer with the Async Attribute
While deferred execution is the default, it is possible to override
this behavior by adding the async attribute to a module
script:
<script type="module" src="analytics.js" async></script>When async is added, the module and its dependency tree
still download in the background without blocking the parser. However,
the script will execute immediately once all required files in the
dependency graph are fetched, regardless of whether the HTML parser has
finished or what order the script appears in the source code.