Native ES modules normally require browser-resolvable URLs such as ./math.js or /vendor/lib.js. Import maps add a controlled indirection layer so application code can use stable bare specifiers such as app/config without a bundler rewriting imports.

Define a map before modules load

<script type="importmap">
{
  "imports": {
    "app/": "/js/app/",
    "vendor-utils": "/vendor/utils-v3.js"
  }
}
</script>
<script type="module" src="/js/main.js"></script>

Then /js/main.js can use:

import { settings } from "app/config.js";
import { clamp } from "vendor-utils";

The browser resolves the specifier through the map before fetching the module.

Use prefix mappings deliberately

A key ending in / maps a specifier prefix. Its target must also end in /. This is useful for a family of internal modules, but avoid mapping overly broad prefixes that make dependencies hard to locate.

Exact mappings are clearer for third-party entry points because version changes happen in one place.

Use scopes when versions must differ

Import maps support scopes, allowing imports under one URL subtree to resolve differently from imports elsewhere. This can help during gradual migrations where two application areas need different dependency versions.

That flexibility has a cost: resolution becomes less obvious. Prefer one version unless a staged migration genuinely requires multiple mappings.

Understand what import maps do not provide

Import maps resolve module names. They do not minify code, remove dead code across arbitrary build pipelines, compile TypeScript, transform JSX, optimize images, or generate legacy bundles.

A bundler remains valuable when those build-time transformations matter. Import maps are strongest for applications that already ship standards-based modules or for server-driven systems that want explicit URL-level dependency control.

Cache versioned targets

Map stable logical names to immutable, versioned assets where possible:

{
  "imports": {
    "charts": "/assets/charts.4f1c9d2.js"
  }
}

The HTML can change the mapping during a deployment while the hashed asset receives a long cache lifetime.

Common pitfalls

Loading the map too late

The browser needs the import map before dependent modules are resolved. Place it before module scripts that rely on its entries.

Mapping to mutable third-party URLs

A remote URL that changes unexpectedly can break reproducibility. Pin versions and use a deployment process that you control.

Assuming aliases work outside the browser

Node.js, test runners, editors, and bundlers have their own resolution rules. Configure those environments separately if the same source is executed there.

Hiding architecture behind aliases

Short names are convenient, but dozens of magical mappings make ownership unclear. Use names that reveal module boundaries.

Choose import maps for resolution, not transformation

Import maps are a small, focused browser primitive. They are useful when the main problem is translating stable module names into deployable URLs. If the project also needs extensive source transformation or packaging, keep the bundler and treat import maps as optional infrastructure rather than a replacement for the entire build system.