Browsers run ES modules natively, with no build step.
<script type="module" src="./app.js"></script>
import { greet } from "./greet.js";
document.querySelector("#out").textContent = greet("Maya");
What type=”module” changes
- The script is deferred automatically: it runs after the HTML is parsed (like
defer), so the elements exist. - It runs in strict mode, with its own scope.
- Each module is loaded once, and its imports are fetched in parallel.
- You can use
importandexport. - A
nomoduleattribute (or a classic script) can serve very old browsers, which is rarely needed today.
Paths
Imports in the browser are URLs:
import { a } from "./utils.js"; // same folder
import { b } from "../lib/b.js"; // parent folder
import { c } from "/js/c.js"; // from the site root
import { d } from "https://example.com/d.js"; // absolute URL
Two rules that catch beginners:
- Relative paths must start with
./or../.import x from "utils.js"is treated as a bare specifier (a package name) and fails in a browser. - Include the
.jsextension. Unlike Node.js or some bundlers, browsers do not guess it.
It must be served over http(s)
Modules are fetched with CORS rules, so opening the page from file:///... (double clicking the HTML file) fails with a CORS error in most browsers. Use a local development server:
npx serve .
# or: python -m http.server
# or the "Live Server" extension in your editor
Bare imports and import maps
To write import { x } from "my-library" without a bundler, an import map tells the browser where the name points:
<script type="importmap">
{ "imports": { "my-library": "/js/vendor/my-library.js" } }
</script>
<script type="module">
import { x } from "my-library";
</script>
Several entry points and caching
Browsers cache module files like other scripts. Give files hashed or versioned names (app.3f9c.js) so updates are picked up. Build tools do this for you.
Many small files = many requests
Without a bundler, each module is a separate request. Over HTTP/2 or HTTP/3 this is fine for small and medium projects. For big ones, bundlers combine modules and remove unused code.
Inline modules
<script type="module">
const message = "Inline module code works too";
console.log(message);
</script>
Global variables from modules
Top-level variables are not attached to window. If you must share something, export it, or set it on window explicitly (rarely a good idea).