Native ES Modules in the Browser
Native ES modules give the browser a real module graph with static imports, deferred execution, and per-module caching, replacing bundler-only assumptions.
Definition
An ES module is a JavaScript file whose imports and exports form a static dependency graph that the browser resolves and evaluates, with module code running in strict mode and its own scope.
Context and scope
It underpins modern build tooling, dynamic imports for code splitting, and import maps for bare-specifier resolution.
Common misconceptions
- modules execute in the order their script tags appear
- a module's bindings are copied by value at import time
Anti-patterns
- mixing classic and module scripts without tracking execution order
- assuming server code can load a .js file that the browser treats differently
Related concepts
Authoritative source
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
Definition
An ES module is a JavaScript file whose imports and exports form a static dependency graph that the browser resolves and evaluates, with module code running in strict mode and its own scope.
It underpins modern build tooling, dynamic imports for code splitting, and import maps for bare-specifier resolution. Compared to simpler alternatives, this concept fits best when the trade-off it represents is the one the system actually cares about.
When to use it
A common use case is applying it at the boundary where the cost of getting it wrong is highest, and the scenario usually involves a concrete guarantee the system must uphold. Unlike an ad hoc approach, it makes that guarantee explicit, reviewable, and testable.
How it works
Module scripts are deferred by default, execute once per graph even if imported many times, and evaluate dependencies before their dependents, giving a deterministic order.
Imported bindings are live references rather than copies, so a mutation to an exported variable is observable in the importing module, unlike destructured values.
Trade-offs and alternatives
Bare specifiers such as a package name are not valid URLs in the browser until an import map maps them, and dynamic import returns a promise for code defined in a separate module. The main drawback is the extra complexity it introduces, so the benefit should clearly outweigh that cost before a team adopts it.
Common misconceptions
A persistent misconception is that modules execute in the order their script tags appear. Another is that a module’s bindings are copied by value at import time. For example, an engineer who assumes otherwise may design around a guarantee that does not actually exist, which only becomes obvious under load or failure.
Anti-patterns to avoid
Two recurring anti-patterns are to mixing classic and module scripts without tracking execution order and to assuming server code can load a .js file that the browser treats differently. Both tend to surface as hard-to-debug production issues rather than obvious test failures. Related concepts include Import Maps for Module Resolution, CSS Cascade Layers, CSS Container Queries.