Chapter 1 of ?
js 8 min read

JavaScript Mastery — Chapter 33: JS Modules — ES Modules

Module 8: Formats, Modules & Metaprogramming • Chapter 33

JS Modules — ES Modules

Master native modular architecture: named vs default export, static vs dynamic import(), browser module execution semantics (type="module"), and modern Import Maps.

Static ESM Resolution vs Dynamic Runtime Loading

ES Modules are statically analyzed at compile/parse time. Top-level imports construct an asynchronous dependency graph before any code runs, while dynamic import() enables lazy on-demand code splitting.

Static import './math.js' Parsed BEFORE execution Static Link Module Record Graph Live Read-Only Bindings import('./chart.js') Dynamic Promise at Runtime Lazy Fetched On-Demand Execution Complete Zero Global Leakage

1. Named vs Default Exports & Re-exporting

ES Modules allow two fundamental export flavors. Named exports are statically verified by linters and bundlers, while default exports represent the primary entity of a module file.

Named Exports (Recommended)
// utils.js
export const API_URL = 'https://api.site.com';
export function calculateTax(amount) {
  return amount * 0.15;
}

// consumer.js
import { API_URL, calculateTax as getTax } from './utils.js';
Default Export
// UserProfile.js
export default class UserProfile {
  constructor(name) { this.name = name; }
}

// consumer.js (Can import using any arbitrary name!)
import ProfileComponent from './UserProfile.js';

The Barrel Re-Export Pattern

Group multiple submodule exports into a single clean entrypoint index:

// components/index.js
export { Button } from './Button.js';
export { Modal } from './Modal.js';
export { Card } from './Card.js';

// Application consumption:
import { Button, Modal, Card } from './components/index.js';
Test in Playground

2. Browser Module Execution: type="module"

When you add <script type="module" src="app.js"></script> to your HTML, the browser fundamentally changes its script execution behavior:

Automatic Defer

Module scripts defer automatically: HTML parsing continues uninterrupted, and execution occurs only when the DOM is fully constructed.

Strict Mode & Scoping

"use strict" is on by default. Top-level variables remain module-scoped and never pollute the global window object.

CORS & Singleton

Loaded via CORS (fails on file://). Each module executes exactly once, even if imported by 10 different files.

Modern Import Maps (No Bundler Required)

Import Maps let native browsers resolve "bare specifiers" (like import lodash from 'lodash') directly without Vite or Webpack:

<script type="importmap">
{
  "imports": {
    "lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js",
    "chart": "/js/components/chart.esm.js"
  }
}
</script>

<script type="module">
  import _ from 'lodash'; // Resolves via importmap!
  console.log(_.capitalize('aicodelab'));
</script>
Run in Playground

3. Dynamic import() & Route-Based Code Splitting

Static import ... from ... must appear at the top-level of a module. For route-based lazy loading or conditional features, dynamic import('path') can be called anywhere inside functions and returns a Promise resolving to the module's namespace object.

On-Demand Feature Loading

button.addEventListener('click', async () => {
  // Only downloads heavy PDF library when user clicks export!
  try {
    const { generatePdfReport } = await import('./heavyPdfGenerator.js');
    generatePdfReport(reportData);
  } catch (err) {
    console.error('Failed to load module:', err);
  }
});
Test in Playground
Hands-on Challenge

Build an Extensible Plugin Registry with Dynamic Loading

Implement a PluginRegistry class that accepts a plugin URL, dynamically loads it via import(), ensures it implements required lifecycle hooks (name and init()), and executes initialization.

class PluginRegistry {
  constructor() {
    this.plugins = new Map();
  }

  async loadPlugin(moduleUrl) {
    try {
      const module = await import(moduleUrl);
      const plugin = module.default || module;

      if (!plugin.name || typeof plugin.init !== 'function') {
        throw new Error(`Plugin at ${moduleUrl} does not conform to specification`);
      }

      await plugin.init();
      this.plugins.set(plugin.name, plugin);
      console.log(`Plugin '${plugin.name}' initialized successfully.`);
      return plugin;
    } catch (err) {
      console.error(`Error loading plugin from ${moduleUrl}:`, err.message);
      throw err;
    }
  }
}

Chapter 33 Knowledge Check

Validate your mastery of ES Modules, browser execution semantics, and dynamic loading.

1. Are variables declared at the top level of an ES Module attached to the global window object?
Yes, all top-level variables attach to window
No, ES Modules possess their own isolated Module Scope
Only if declared with var
Yes, unless "use strict" is explicitly added
2. What browser attribute must be added to a <script> tag to execute it as an ES Module?
type="javascript/esm"
type="module"
module="true"
async="module"
3. What does dynamic import('path.js') return?
An HTMLCollection
A Promise that resolves to the module's namespace object
A Web Worker instance
Synchronously returns the exported class
4. If a module file is imported by five different files across your application, how many times is the module's top-level code executed?
Exactly once (ES modules are executed as singletons and cached)
Five times
Once per user click
Throws a DuplicateModuleError
5. What is the primary purpose of an HTML <script type="importmap">?
Compiles TypeScript into JavaScript in the browser
Maps bare module specifiers (e.g. 'lodash') to specific URLs without requiring a build bundler
Minifies JavaScript code before execution
Replaces CSS stylesheets
Done with this chapter?
Mark it complete to track your progress and unlock your certificate.
Next Up
—

Learner Reviews

Write a Review
Share your experience to help other learners.
Your Rating *
★ ★ ★ ★ ★