DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

JavaScript ES Modules: Native Browser Modules, Dynamic Imports, and Import Maps

JavaScript modules (ESM — ECMAScript Modules) are the native module system built into browsers. They let you split code into separate files with explicit imports/exports, enable tree-shaking, and work without a bundler in modern browsers. Firefox has full native ES module support.

The Basics: export and import

// utils/math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }

export const PI = 3.14159265358979;

// Default export (one per file)
export default function calculateArea(radius) {
  return PI * radius * radius;
}
Enter fullscreen mode Exit fullscreen mode
// main.js
import calculateArea, { add, multiply, PI } from './utils/math.js';

// Rename on import
import { add as sum } from './utils/math.js';

// Import all as namespace
import * as Math from './utils/math.js';
Math.add(1, 2);
Math.PI;
Enter fullscreen mode Exit fullscreen mode

Using Modules in HTML

<!-- type="module" enables ES module syntax -->
<script type="module" src="main.js"></script>

<!-- Inline module -->
<script type="module">
  import { add } from './utils/math.js';
  console.log(add(1, 2));
</script>
Enter fullscreen mode Exit fullscreen mode

Key differences with type="module":

  • Strict mode by default
  • this at top level is undefined (not window)
  • Scripts are deferred automatically (no defer needed)
  • Each module executes only once
  • Cross-origin requires proper CORS headers
  • Works with local files only in Firefox via about:config or a local server (CORS blocks file:// protocol for modules)

Dynamic Imports

Load modules on demand, not at startup:

// Import when needed (returns a Promise)
const { default: Chart } = await import('./chart.js');
new Chart(data);

// Conditional import
if (userPreference === 'advanced') {
  const { AdvancedFeatures } = await import('./advanced.js');
  AdvancedFeatures.init();
}

// Load multiple at once
const [{ utils }, { components }] = await Promise.all([
  import('./utils.js'),
  import('./components.js'),
]);
Enter fullscreen mode Exit fullscreen mode

Dynamic imports enable code splitting — only load code when it's needed.

Import Maps

Tell the browser where to find module specifiers (like Node.js package names):

<script type="importmap">
{
  "imports": {
    "lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4/lodash.js",
    "react": "https://esm.sh/react@18",
    "react-dom/client": "https://esm.sh/react-dom@18/client",
    "components/": "/src/components/"
  }
}
</script>

<script type="module">
  import _ from 'lodash'; // Resolved by import map
  import React from 'react';
  import { Button } from 'components/Button.js';
</script>
Enter fullscreen mode Exit fullscreen mode

Import maps let you use bare specifiers without a bundler. Firefox 108+ supports import maps.

Module Patterns

Singleton

// db.js
let instance = null;

export function getDB() {
  if (!instance) {
    instance = createDatabaseConnection();
  }
  return instance;
}
Enter fullscreen mode Exit fullscreen mode

Modules are cached — getDB() always returns the same instance.

Re-exporting

// index.js (barrel file)
export { Button } from './Button.js';
export { Modal } from './Modal.js';
export { Form, Input, Select } from './Form.js';
Enter fullscreen mode Exit fullscreen mode
// Consumer
import { Button, Modal } from './components/index.js';
Enter fullscreen mode Exit fullscreen mode

Factory Pattern

// logger.js
export function createLogger(prefix) {
  return {
    log: (msg) => console.log(`[${prefix}] ${msg}`),
    error: (msg) => console.error(`[${prefix}] ${msg}`),
    warn: (msg) => console.warn(`[${prefix}] ${msg}`),
  };
}
Enter fullscreen mode Exit fullscreen mode

Circular Dependencies

ESM handles circular imports — but you need to be careful about initialization order:

// a.js
import { b } from './b.js';
export const a = 'A + ' + b; // b might not be initialized yet!

// b.js
import { a } from './a.js';
export const b = 'B + ' + a;
Enter fullscreen mode Exit fullscreen mode

When circular, one module's exports might be undefined on first use. The fix: use functions that are called after all modules initialize:

// a.js
import { getB } from './b.js';
export function getA() { return 'A + ' + getB(); } // Called later, b is ready

// b.js
import { getA } from './a.js';
export function getB() { return 'B + ' + getA(); }
Enter fullscreen mode Exit fullscreen mode

Modules vs Scripts

Classic Scripts ES Modules
Execution Immediately Deferred
Top-level this window undefined
Strict mode Opt-in Always
Duplicate execution Each <script> runs Cached, run once
CORS required No Yes (for cross-origin)
import/export No Yes

Using Modules Without a Bundler

For small projects, modules work natively in Firefox without any build step:

/my-app
  index.html
  main.js
  utils/
    math.js
    dom.js
  components/
    button.js
    modal.js
Enter fullscreen mode Exit fullscreen mode
<!-- index.html -->
<script type="module" src="main.js"></script>
Enter fullscreen mode Exit fullscreen mode

Serve with any local HTTP server:

npx serve .
# or
python3 -m http.server 8080
Enter fullscreen mode Exit fullscreen mode

Firefox loads all modules via HTTP — no bundler needed for small apps.

Module Workers

// Main thread
const worker = new Worker('/workers/processor.js', { type: 'module' });

// workers/processor.js
import { processData } from '../utils/processing.js'; // ES imports work!

self.onmessage = async (event) => {
  const result = await processData(event.data);
  self.postMessage(result);
};
Enter fullscreen mode Exit fullscreen mode

Module workers support import — no global scope pollution.

DevTools: Module Inspection

In Firefox Debugger:

  1. All loaded modules appear in the file tree
  2. Dynamic imports show up as new files when they load
  3. Breakpoints work in imported modules
  4. The "Sources" column shows which file a script came from, including the full module path

Top comments (0)