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

> Discover the key differences between JavaScript modules and traditional script tags Understand isolated scopes asynchronous loading and explicit imports versus global namespace pollution Explore modern JavaScript development

- Repository: [Kyle Simpson/You-Dont-Know-JS](https://github.com/getify/You-Dont-Know-JS)
- Tags: deep-dive
- Published: 2026-02-24

---

**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`](https://github.com/getify/You-Dont-Know-JS/blob/main/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`](https://github.com/getify/You-Dont-Know-JS/blob/main/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`](https://github.com/getify/You-Dont-Know-JS/blob/main/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)

```html
<!-- index.html -->
<script src="utils.js"></script>
<script src="app.js"></script>

```

```javascript
/* 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)

```html
<!-- index.html -->
<script type="module" src="app.js"></script>

```

```javascript
/* 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`](https://github.com/getify/You-Dont-Know-JS/blob/main/utils.js) exports only what it wants to expose
- [`app.js`](https://github.com/getify/You-Dont-Know-JS/blob/main/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`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch8.md), both achieve similar goals but ES modules provide the standard syntax for browsers and modern tooling.

```javascript
// commonjs.js (Node, CommonJS)
function greet(name) {
  return `Hello, ${name}!`;
}
module.exports = { greet };

```

```javascript
// 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`](https://github.com/getify/You-Dont-Know-JS/blob/main/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.