von bare Specifiers bis zum Multi-Version-Scope
Import Maps loesen ein Problem, das ES Module im Browser seit ihrer Einfuehrung hatten: bare Specifiers wie import { render } from "preact" funktionieren nativ nicht, ohne dass ein Build-Schritt den Pfad umschreibt. Mit einer Import Map traegt der Browser selbst die Aufloesung, Scopes erlauben sogar unterschiedliche Versionen fuer verschiedene Modulbereiche, ganz ohne Webpack oder Vite.
Inhaltsverzeichnis
- 1. Welches Problem Import Maps wirklich loesen
- 2. Grundsyntax: das script-Element vom Typ importmap
- 3. Bare Specifiers aufloesen: von Paketname zu URL
- 4. Scopes: unterschiedliche Versionen pro Modulbereich
- 5. Import Maps mit CDN-Modulen kombinieren
- 6. Integrity und Sicherheit bei fremden Quellen
- 7. Browser-Support und Fallback mit es-module-shims
- 8. Import Maps in Produktion: Cache-Busting und Generierung
- 9. Import Maps im Vergleich zu Bundler-Aliasing
- 10. Zusammenfassung
- 11. FAQ
1. Welches Problem Import Maps wirklich loesen
Native ES Module im Browser kennen nur zwei Arten von Import-Pfaden: relative Pfade wie ./modul.js und absolute URLs wie https://example.com/modul.js. Ein sogenannter bare Specifier, also import { render } from "preact" ohne Pfadangabe, funktioniert ohne weitere Hilfe nicht. Genau dieses Verhalten hat jahrelang dafuer gesorgt, dass jedes Frontend-Projekt automatisch einen Bundler brauchte, selbst wenn die Anwendung selbst kein Code-Splitting oder Tree-Shaking benoetigte. Import Maps schliessen diese Luecke direkt in der Browser-Engine.
Eine Import Map ist im Kern eine JSON-Struktur, die dem Browser mitteilt, wie bare Specifiers zu tatsaechlichen URLs aufgeloest werden. Statt dass ein Bundler beim Build jeden Import-Pfad umschreibt, uebernimmt der Browser diese Aufloesung zur Laufzeit, direkt beim Parsen des Moduls. Das macht Import Maps besonders interessant fuer Projekte, die bewusst ohne Build-Schritt arbeiten wollen: interne Admin-Tools, Prototypen, aber auch Produktionscode, der auf HTTP/2-Multiplexing statt auf ein einziges grosses Bundle setzt.
Wichtig ist die Abgrenzung: Import Maps ersetzen keinen vollwertigen Bundler mit Tree-Shaking, Minifizierung oder Code-Splitting. Sie loesen ausschliesslich das Mapping-Problem, also die Frage, wohin ein Modulname zeigt. Wer zusaetzlich Transpilierung fuer aeltere Browser oder aggressive Bundle-Optimierung braucht, kombiniert Import Maps haeufig trotzdem mit einem leichten Build-Schritt, verliert dabei aber nicht die Kontrolle ueber die eigentliche Modulaufloesung.
2. Grundsyntax: das script-Element vom Typ importmap
Eine Import Map wird ueber ein <script type="importmap">-Element eingebunden, das vor dem ersten <script type="module"> im Dokument stehen muss. Der Browser liest die Map, sobald das Dokument geparst wird, und wendet sie auf alle folgenden Modul-Imports an. Es kann pro Dokument nur eine einzige Import Map geben, mehrere importmap-Elemente fuehren zu einem Fehler, das ist eine bewusste Design-Entscheidung der Spezifikation gegen widerspruechliche Mappings.
Die Grundstruktur besteht aus dem Schluessel imports, einem Objekt, das Specifier auf URLs abbildet. Ein Trailing Slash im Specifier signalisiert dem Browser, dass alles, was danach folgt, an die Ziel-URL angehaengt wird, das ist das sogenannte Prefix-Mapping und besonders nuetzlich fuer ganze Paket-Verzeichnisse mit mehreren Untermodulen.
<!-- Import Map muss VOR dem ersten module-script stehen -->
<script type="importmap">
{
"imports": {
"preact": "/vendor/preact/preact.module.js",
"preact/hooks": "/vendor/preact/hooks.module.js",
"lodash-es/": "/vendor/lodash-es/",
"@app/": "/src/app/"
}
}
</script>
<script type="module">
// Bare specifier wird ueber die Import Map aufgeloest
import { h, render } from "preact";
import { useState } from "preact/hooks";
// Prefix-Mapping: alles nach "lodash-es/" wird angehaengt
import debounce from "lodash-es/debounce.js";
// Eigenes Alias-Prefix fuer Anwendungscode
import { initRouter } from "@app/router.js";
render(h("div", null, "Hallo Import Map"), document.body);
</script>
Ein haeufiger Anfaengerfehler ist, den Trailing Slash zu vergessen. "lodash-es": "/vendor/lodash-es/" ohne Slash im Key bildet nur den exakten Specifier lodash-es ab, nicht aber lodash-es/debounce.js. Erst "lodash-es/": "/vendor/lodash-es/" mit Slash auf beiden Seiten aktiviert das Prefix-Verhalten und erlaubt beliebige Untermodule aus demselben Paket.
3. Bare Specifiers aufloesen: von Paketname zu URL
Die Kernfunktion einer Import Map ist die Aufloesung von Specifiern in konkrete Modul-URLs, und der Aufloesungs-Algorithmus folgt einer klaren Prioritaet: exakte Treffer im imports-Objekt gewinnen immer vor Prefix-Treffern. Gibt es sowohl einen Eintrag fuer "preact" als auch fuer "preact/", wird fuer den exakten Specifier preact der spezifische Eintrag verwendet, waehrend preact/compat ueber das Prefix aufgeloest wird. Diese Praezedenz macht Import Maps vorhersagbar, auch wenn mehrere Regeln theoretisch zutreffen koennten.
Fuer TypeScript-Projekte, die ohne Bundler arbeiten, ist die Kombination aus Import Maps und nativer Typpruefung besonders praktisch: der TypeScript-Compiler kennt zwar keine Import Maps direkt, aber ueber das paths-Mapping in tsconfig.json laesst sich dieselbe Struktur spiegeln, sodass Editor-Autovervollstaendigung und Laufzeit-Aufloesung konsistent bleiben. Das erspart die sonst uebliche Diskrepanz zwischen dem, was der Editor anzeigt, und dem, was der Browser tatsaechlich laedt.
Ein weiterer praktischer Anwendungsfall: mehrere Teams in einem grossen Frontend nutzen denselben Specifier fuer interne Bibliotheken, etwa @shared/ui. Ohne Import Maps muesste jedes Modul den vollen Pfad kennen und bei einer Umstrukturierung aendern. Mit einer zentralen Import Map genuegt eine einzige Aenderung an einer Stelle, alle Module, die @shared/ui importieren, folgen automatisch dem neuen Zielort.
4. Scopes: unterschiedliche Versionen pro Modulbereich
Der scopes-Schluessel ist die maechtigste Funktion von Import Maps und loest ein Problem, das klassische Bundler-Konfigurationen nur ueber verschachtelte node_modules-Verzeichnisse abbilden: unterschiedliche Modulversionen fuer unterschiedliche Teile derselben Anwendung. Ein Scope ist an einen URL-Pfadpraefix gebunden, innerhalb dieses Praefixes gelten eigene Mapping-Regeln, die die globalen imports-Eintraege ueberschreiben.
Das klassische Szenario: ein Legacy-Widget im Verzeichnis /widgets/legacy/ braucht zwingend eine aeltere Version einer Utility-Bibliothek, waehrend der Rest der Anwendung bereits auf die neue Major-Version migriert ist. Ohne Scopes muessten beide Versionen unter unterschiedlichen Specifiern importiert werden, was den Code an jeder Importstelle verkompliziert. Mit einem Scope importiert das Legacy-Widget denselben Specifier wie der Rest der App, bekommt aber automatisch die passende URL ausgeliefert.
<script type="importmap">
{
"imports": {
"date-utils": "/vendor/date-utils/v3/index.js"
},
"scopes": {
"/widgets/legacy/": {
"date-utils": "/vendor/date-utils/v1/index.js"
}
}
}
</script>
<script type="module">
// Ausserhalb von /widgets/legacy/: v3 wird geladen
import { formatDate } from "date-utils";
</script>
<!-- widgets/legacy/panel.js laeuft im Scope /widgets/legacy/ -->
<!-- dort wird "date-utils" automatisch auf v1 aufgeloest -->
Scopes greifen nur fuer Module, deren eigene URL innerhalb des Scope-Praefixes liegt, nicht fuer Module, die von dort aus importiert werden, aber selbst ausserhalb liegen. Diese Feinheit sorgt oft fuer Verwirrung: ein Modul in /widgets/legacy/panel.js, das seinerseits ein Modul aus /vendor/shared.js importiert, nutzt fuer diesen zweiten Import wieder die globalen imports-Regeln, nicht den Scope, in dem panel.js selbst liegt. Mehrere ineinander verschachtelte Scopes werden nach Spezifitaet des Pfadpraefixes aufgeloest, der laengste passende Praefix gewinnt.
5. Import Maps mit CDN-Modulen kombinieren
ESM-CDNs wie esm.sh, jspm.io oder unpkg.com liefern npm-Pakete bereits als native ES Module aus, inklusive aufgeloester Abhaengigkeiten. In Kombination mit Import Maps entsteht daraus ein voll funktionsfaehiges Frontend ganz ohne lokale node_modules und ohne Build-Schritt: die Import Map zeigt auf die CDN-URLs, der Browser laedt den Rest selbst.
Der grosse Vorteil gegenueber direkten CDN-URLs im Code ist die Entkopplung: Anwendungscode importiert weiterhin "react", nicht "https://esm.sh/react@18.3.1". Ein Versionswechsel oder ein Wechsel des CDN-Anbieters bedeutet eine einzige Aenderung in der Import Map, nicht in jeder einzelnen Datei, die React importiert. Das ist genau dieselbe Entkopplung, die man von package.json in einem Node-Projekt kennt, nur ohne Package Manager.
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.3.1",
"react-dom/client": "https://esm.sh/react-dom@18.3.1/client",
"zustand": "https://esm.sh/zustand@4.5.2"
}
}
</script>
<script type="module">
import { createElement as h } from "react";
import { createRoot } from "react-dom/client";
import { create } from "zustand";
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
createRoot(document.getElementById("app")).render(
h("button", { onClick: useStore.getState().increment }, "Klick mich")
);
</script>
Fuer Produktionsanwendungen empfiehlt es sich, CDN-Versionen strikt zu pinnen, also niemals @latest zu verwenden. Ein CDN-Update ausserhalb der eigenen Kontrolle darf niemals unerwartet in die Produktion durchschlagen. Manche Teams spiegeln CDN-Module zusaetzlich auf eigene Infrastruktur, um Verfuegbarkeit und Latenz unter eigener Kontrolle zu behalten, waehrend die Import Map selbst unveraendert bleibt.
6. Integrity und Sicherheit bei fremden Quellen
Sobald eine Import Map auf fremde Domains zeigt, wird Subresource Integrity relevant. Seit den neueren Iterationen der Spezifikation unterstuetzen Import Maps ein integrity-Objekt, das pro URL einen Hash hinterlegt, den der Browser vor der Ausfuehrung prueft. Weicht der tatsaechlich ausgelieferte Inhalt vom Hash ab, etwa weil ein CDN kompromittiert wurde, verweigert der Browser das Laden des Moduls, statt manipulierten Code stillschweigend auszufuehren.
Ergaenzend greift die Content Security Policy: eine script-src-Direktive, die externe Domains nicht explizit erlaubt, blockiert auch CDN-Module, die ueber die Import Map eingebunden werden. Wer strikte CSP-Regeln fahren will, sollte die tatsaechlich genutzten CDN-Domains gezielt whitelisten, statt unsafe-inline oder eine zu breite Wildcard-Regel zu verwenden. Diese Kombination aus Integrity-Hashes und CSP macht Import Maps auch fuer sicherheitskritische Anwendungen praktikabel, sofern beide Mechanismen konsequent eingesetzt werden.
7. Browser-Support und Fallback mit es-module-shims
Chrome und Edge unterstuetzen Import Maps seit Version 89, Firefox seit Version 108 und Safari seit Version 16.4, Scopes und das integrity-Feld kamen in einigen Browsern etwas spaeter dazu als das Grundfeature. Fuer Anwendungen, die noch aeltere Browser-Versionen unterstuetzen muessen, existiert mit es-module-shims ein gut gepflegtes Polyfill, das Import Maps per JavaScript nachbildet, inklusive Scopes und dynamischem import().
Das Polyfill funktioniert, indem es Module selbst per fetch laedt, den Quelltext analysiert und Specifier vor der Ausfuehrung umschreibt. Das kostet etwas Performance gegenueber der nativen Browser-Implementierung, ist aber fuer die meisten Anwendungen unproblematisch, solange man das Polyfill nur dort einbindet, wo es tatsaechlich gebraucht wird. Ein Feature-Test vor dem Laden des Polyfills verhindert unnoetigen Overhead in modernen Browsern.
<!-- Polyfill nur laden, wenn native Import Maps fehlen -->
<script>
if (!HTMLScriptElement.supports || !HTMLScriptElement.supports("importmap")) {
document.write(
'<script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js"><\/script>'
);
}
</script>
<script type="importmap">
{
"imports": {
"preact": "https://esm.sh/preact@10.22.0"
}
}
</script>
<!-- type="module-shim" statt "module" fuer garantiertes Polyfill-Verhalten -->
<script type="module-shim">
import { h, render } from "preact";
render(h("p", null, "Funktioniert mit und ohne native Import Map"), document.body);
</script>
Wichtig ist die Reihenfolge: der Feature-Test muss vor dem importmap-Script laufen, sonst laedt der Browser das Polyfill zu spaet, nachdem bereits ein Fehler beim Aufloesen eines Moduls aufgetreten ist. Manche Teams entscheiden sich bewusst gegen den Feature-Test und laden es-module-shims immer, um Verzweigungslogik im HTML zu vermeiden, das kostet aber in modernen Browsern unnoetig Ladezeit.
8. Import Maps in Produktion: Cache-Busting und Generierung
In Produktion sollte eine Import Map nicht von Hand gepflegt werden, sobald mehr als eine Handvoll Module beteiligt sind. Ein kleines Build-Skript, das Verzeichnisse scannt und die Import Map automatisch generiert, verhindert veraltete Eintraege und macht Versionswechsel reproduzierbar. Fuer Cache-Busting traegt man Content-Hashes direkt in die Ziel-URLs der Import Map ein, genau wie man es von Bundler-Ausgaben mit Hash im Dateinamen kennt.
// generate-importmap.mjs — builds importmap.json from a manifest
import { createHash } from "node:crypto";
import { readFile, writeFile, readdir } from "node:fs/promises";
import path from "node:path";
const VENDOR_DIR = "./public/vendor";
async function hashFile(filePath) {
const content = await readFile(filePath);
return createHash("sha256").update(content).digest("hex").slice(0, 8);
}
async function buildImportMap() {
const entries = await readdir(VENDOR_DIR, { withFileTypes: true });
const imports = {};
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".js")) continue;
const fullPath = path.join(VENDOR_DIR, entry.name);
const hash = await hashFile(fullPath);
const specifier = entry.name.replace(/\.js$/, "");
// Cache-busting hash in the served URL, not the local filename
imports[specifier] = `/vendor/${entry.name}?v=${hash}`;
}
const importMap = { imports };
await writeFile("./public/importmap.json", JSON.stringify(importMap, null, 2));
console.log(`Import Map mit ${Object.keys(imports).length} Eintraegen erzeugt`);
}
buildImportMap();
Diese generierte Import Map laesst sich per fetch laden und dynamisch als script-Element einfuegen, bevor das erste modulare Script ausgefuehrt wird, oder serverseitig direkt ins HTML injizieren. Fuer die serverseitige Variante bietet sich ein Template-System an, das die JSON-Datei zur Build-Zeit ausliest und inline einbettet, das spart einen zusaetzlichen Netzwerk-Roundtrip beim ersten Seitenaufruf und ist die in der Praxis empfohlene Vorgehensweise.
9. Import Maps im Vergleich zu Bundler-Aliasing
Bundler wie Webpack oder Vite loesen das Mapping-Problem seit Jahren ueber Alias-Konfigurationen, aber der grundlegende Unterschied liegt im Zeitpunkt der Aufloesung: Bundler schreiben Importpfade beim Build um, Import Maps loesen sie zur Laufzeit im Browser auf. Das hat direkte Konsequenzen fuer Entwicklungsgeschwindigkeit, Debugging und Deployment-Flexibilitaet.
| Kriterium | Bundler-Aliasing | Import Maps | Praxis-Hinweis |
|---|---|---|---|
| Zeitpunkt der Aufloesung | Build-Zeit, statisch verdrahtet | Laufzeit, im Browser | Import Maps aendern ohne Rebuild |
| Build-Schritt noetig | Ja, immer | Optional | Import Maps passen zu Build-freien Setups |
| DevTools-Debugging | Sourcemaps noetig fuer Original-Code | Original-Dateien direkt sichtbar | Kein Mapping-Overhead beim Debuggen |
| Multi-Version-Support | Verschachtelte node_modules | Scopes, explizit konfigurierbar | Scopes sind expliziter als Auflösungsalgorithmen |
| Tree-Shaking | Ja, automatisch | Nein, nicht enthalten | Fuer grosse Apps bleibt ein Bundler sinnvoll |
In der Praxis ist die Entscheidung selten binaer: viele Teams nutzen Import Maps fuer Vendor-Abhaengigkeiten und einen leichten Bundler ausschliesslich fuer den eigenen Anwendungscode, dort wo Tree-Shaking den groessten Effekt hat. Diese Hybrid-Strategie kombiniert die Vorteile beider Ansaetze, ohne sich vollstaendig fuer die eine oder andere Seite entscheiden zu muessen.
Mironsoft
Modernes JavaScript-Tooling und Frontend-Architektur
Weniger Build-Komplexitaet, mehr native Browser-Features?
Wir pruefen, wo Import Maps euren Build-Schritt schlanker machen koennen und richten CDN-Module, Scopes und Fallback-Strategien fuer produktionstaugliche Setups ein.
Architektur-Review
Bewertung, wo Import Maps euren Build-Prozess vereinfachen
Migration
Schrittweiser Umstieg auf native Module ohne Risiko fuer den Produktivbetrieb
Tooling-Setup
Automatisierte Import-Map-Generierung mit Cache-Busting fuer euer Deployment
10. Zusammenfassung
Import Maps loesen ein sehr konkretes Problem: bare Specifiers, die native ES Module im Browser nicht ohne Hilfe verstehen. Ueber ein <script type="importmap">-Element weist man Specifier auf konkrete URLs, mit Trailing Slash sogar ganze Paketpraefixe. Scopes erlauben unterschiedliche Modulversionen fuer unterschiedliche Bereiche derselben Anwendung, ein Problem, das klassische Bundler nur ueber verschachtelte Verzeichnisstrukturen loesen. In Kombination mit ESM-CDNs ermoeglichen Import Maps voll funktionsfaehige Frontends ganz ohne lokalen Build-Schritt.
Fuer Produktivsysteme empfiehlt sich strikte Versionsfixierung, Subresource Integrity fuer fremde Quellen und ein automatisiertes Skript zur Generierung der Import Map statt manueller Pflege. Wo aeltere Browser unterstuetzt werden muessen, schliesst es-module-shims die Luecke zuverlaessig. Import Maps ersetzen keinen vollwertigen Bundler mit Tree-Shaking, sind aber fuer viele Projekte der einfachere, direktere Weg, moderne JavaScript-Module auszuliefern.
Import Maps — Das Wichtigste auf einen Blick
Grundprinzip
<script type="importmap"> bildet bare Specifiers auf URLs ab, aufgeloest zur Laufzeit direkt im Browser.
Scopes fuer Multi-Version
Pfadgebundene Regeln ueberschreiben globale Mappings, ideal fuer Legacy-Widgets mit alten Abhaengigkeiten.
Sicherheit
Integrity-Hashes und eine passende CSP-Direktive schuetzen vor manipulierten CDN-Inhalten.
Fallback
es-module-shims repliziert Import Maps per JavaScript fuer aeltere Browser, per Feature-Test nur bei Bedarf laden.