How JavaScript Modules Differ from Traditional Script Tags: A Complete Guide

JavaScript modules run in isolated private scopes with asynchronous loading and explicit import/export mechanisms, while traditional script tags execute synchronously in the global scope, polluting the global namespace with all top-level declarations.

Understanding how JavaScript modules differ from traditional script tags is essential for modern web development. In the You-Dont-Know-JS repository, Kyle Simpson explains that while classic <script> tags execute code in the global scope, ES modules (ESM) introduced in ES2015 provide encapsulation, dependency management, and cleaner architecture. These architectural differences fundamentally change how developers structure applications and manage code dependencies.

Scope Isolation and the Global Namespace

Traditional <script> tags execute in the global scope, meaning every var, function, or let declaration at the top level becomes a property of the global object (window in browsers). This creates a high risk of naming collisions when loading multiple scripts, as every variable competes for space in the single shared namespace.

JavaScript modules operate with module scope. According to scope-closures/ch4.md, loading a file via <script type="module"> treats the entire file as a module, isolating its top-level identifiers from the global scope. Variables and functions remain private unless explicitly exported, preventing accidental interference between separate codebases.

Loading Behavior and Execution Order

Traditional scripts are fetched and executed synchronously in the order they appear in the document. The browser blocks HTML parsing while downloading and executing each script, which can create performance bottlenecks and race conditions when dependencies are not loaded in the correct sequence.

Modules load asynchronously and respect a declarative dependency graph. The browser or runtime resolves all imports before executing any module code. As noted in get-started/ch2.md, ES modules have been supported in browsers for several years, enabling non-blocking module resolution that allows parallel fetching and proper dependency ordering that traditional tags cannot guarantee.

Explicit Exports and Imports

Without modules, sharing code requires attaching properties to the global window object or using manual script concatenation. This implicit dependency management makes it difficult to track which files depend on others, leading to fragile load-order dependencies and "spaghetti" code architectures.

ES modules use the export and import keywords to create explicit contracts between files. As detailed in scope-closures/ch8.md, the export keyword exposes specific identifiers while import declares dependencies, providing a clear API surface that modern bundlers can analyze for tree-shaking to eliminate unused code.

Strict Mode and Singleton Behavior

Modules enforce strict mode automatically without requiring the "use strict" pragma. This prevents common mistakes like assigning to undeclared variables, using octal literals, or accidentally creating global variables through typos.

Additionally, modules act as singletons per file. The first import of a module creates an instance, and subsequent imports receive the same instance with shared state. Traditional scripts run once per tag, but their state lives globally rather than being encapsulated within a module instance, making state management harder to control across large applications.

Practical Code Examples

Traditional Script Tags (Global Scope)

<!-- index.html -->
<script src="utils.js"></script>
<script src="app.js"></script>
/* utils.js */
function add(a, b) {
  return a + b;
}
window.add = add;   // exposed globally

/* app.js */
console.log(add(2, 3)); // works because `add` is global

All functions become globals, which can lead to name collisions and makes dependency tracking impossible.

ES Modules (Module Scope)

<!-- index.html -->
<script type="module" src="app.js"></script>
/* utils.js (module) */
export function add(a, b) {
  return a + b;
}

/* app.js (module) */
import { add } from "./utils.js";

console.log(add(2, 3)); // clean import, no globals
  • utils.js exports only what it wants to expose
  • app.js explicitly imports add, keeping the global namespace untouched
  • Dependencies are statically analyzable for tooling and optimization

CommonJS vs ES Modules in Node.js

While Node.js historically used CommonJS (module.exports and require), modern JavaScript favors ES modules. As shown in scope-closures/ch8.md, both achieve similar goals but ES modules provide the standard syntax for browsers and modern tooling.

// commonjs.js (Node, CommonJS)
function greet(name) {
  return `Hello, ${name}!`;
}
module.exports = { greet };
// esmodule.mjs (Node, ES Module)
export function greet(name) {
  return `Hello, ${name}!`;
}

Summary

  • Scope isolation: Modules run in private scope; traditional scripts pollute the global namespace with all top-level declarations
  • Loading mechanism: Modules load asynchronously with dependency graph resolution; scripts load synchronously in document order
  • API design: Modules use explicit export/import syntax; scripts rely on global attachment or manual concatenation
  • Strict mode: Modules enforce strict mode automatically; scripts require manual "use strict" opt-in
  • Tooling support: Modules enable tree-shaking and static analysis; traditional scripts resist optimization and dead-code elimination

Frequently Asked Questions

Do JavaScript modules have access to the global window object?

While modules run in their own scope, they can still access the global window object in browsers or globalThis in any environment. However, top-level declarations inside the module do not automatically become properties of the global object, preventing accidental namespace pollution while still allowing intentional global access when necessary.

Can I mix traditional script tags with module scripts in the same HTML file?

Yes, browsers support mixing both approaches, though it requires careful management of execution order. Traditional scripts execute immediately and synchronously, while module scripts wait for the dependency graph to resolve. Variables declared in traditional tags are available globally, but modules must explicitly import them or access them via window properties if they need to share data.

Why do modules load asynchronously even when placed in the document head?

The ES module system performs static analysis to resolve the entire module graph before execution, as implemented in modern JavaScript engines. This deferred execution ensures all dependencies are available and prevents the blocking behavior associated with synchronous script tags, improving page load performance and preventing race conditions.

Are ES modules supported in all modern browsers?

ES modules have been supported in all major browsers for several years, as documented in get-started/ch2.md. For legacy environments or older Node.js versions, build tools like Webpack, Rollup, or Vite can transpile module syntax into traditional script patterns while maintaining the module abstraction during development.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →