Pros And Cons Of Including Scripts as Modules - ie type="module">>
What changes when you add type="module"?
- Top‑level
awaitworks in modern browsers. - Module scope: top‑level bindings aren’t put on
window. - Deferred by default: module scripts execute after HTML parsing (like
defer). - Strict mode is automatic, no
"use strict"needed. - Imports/exports become available.
Classic script
<script>
// No top-level await
// Globals leak onto window
console.log(window.foo); // undefined
var foo = 42;
console.log(window.foo); // 42
</script>
Module script
<script type="module">
// Works:
const data = await fetch('/api/info').then(r => r.json());
// No global leak:
var foo = 42;
console.log(window.foo); // undefined
</script>
Downsides & differences to be aware of
- Isolation: other scripts can’t “see” your variables unless you explicitly export/import or assign to
window. - Timing: because modules are deferred, code won’t run until HTML is parsed. If you relied on blocking execution, this changes behaviour.
- Multiple modules ≠ shared scope: two
<script type="module">tags are separate modules. - Imports need correct headers: if importing from another file or origin, ensure proper
Content-Typeand CORS. - Legacy browsers: IE11 and very old browsers don’t support modules; top‑level
awaitrequires relatively recent versions.
When it’s a good idea
- Your code is modern‑only and doesn’t need IE/very old Safari/Android browsers.
- You don’t depend on globals leaking to
window. - You’re happy with deferred execution after HTML parsing.
- You want simpler async usage via top‑level
await.
Useful patterns
1) Keep a “main” feel, but with top‑level await
<script type="module">
// Top-level await for bootstrap:
const config = await fetch('/config.json').then(r => r.json());
// Then run your app:
startApp(config);
function startApp(cfg){
const el = document.getElementById('app');
el.textContent = `Ready: ${cfg.name}`;
}
</script>
2) Intentionally expose a global (if you must)
<script type="module">
// Scoped by default:
function greet(name){ return `Hello, ${name}`; }
// Make it global on purpose:
window.greet = greet;
</script>
<script>
// Classic scripts can now use it:
console.log(greet('world'));
</script>
3) Share between modules using imports/exports
// utils.js
export const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// app.js
import { sleep } from './utils.js';
await sleep(200);
console.log('awake');
<script type="module" src="/app.js"></script>
4) Feature-detect modules (very old fallback)
<script type="module">
// modern path
import('/app.js');
</script>
<script nomodule>
// fallback path for very old browsers
// (Serve a transpiled bundle here)
<script src="/legacy-bundle.js"></script>
</script>
FAQ
Does type="module" make my script slower?
Generally no. It changes execution timing (deferred), not performance. Top‑level await pauses your module until the awaited work completes—as intended.
Can I still run code as soon as possible?
Yes—place the module near the end of <body>, or keep it in <head> and rely on the default defer-like behavior. For DOM‑dependent work, waiting for parsing is often ideal.
What about CORS and MIME types?
When importing files (especially cross‑origin), ensure they’re served with Content-Type: application/javascript and that the server allows CORS where needed.

















