Dynamic Import: Muster im Detail fuer Code-Splitting
AI generated
JS
() =>
JavaScript · Code-Splitting · Web Performance
Dynamic Import: Muster im Detail fuer Code-Splitting
von der Route-basierten Aufteilung bis zum Retry bei Chunk-Fehlern

Dynamic Import mit import() ist mehr als eine bequeme Alternative zu statischen Imports: die Funktion gibt ein Promise zurueck, laedt Code erst zur Laufzeit und ermoeglicht Muster wie Route-basiertes Code-Splitting, bedingtes Laden nach Feature Detection und robuste Retry-Strategien, wenn ein Chunk im Netzwerk verloren geht.

17 Min. Lesezeit import() · Code-Splitting · Retry · Preloading Alle modernen Browser · Node.js 14+

1. Was Dynamic Import wirklich macht

Dynamic Import, also der Aufruf import() als Funktion statt als Deklaration, unterscheidet sich fundamental vom statischen import-Statement. Statische Imports werden vor der Ausfuehrung analysiert und bilden einen festen Modulgraphen, waehrend import() zur Laufzeit an einer beliebigen Stelle im Code aufgerufen werden kann und ein Promise zurueckgibt, das mit dem Modul-Namespace-Objekt aufgeloest wird. Diese Laufzeit-Natur macht Dynamic Import zum zentralen Baustein fuer Code-Splitting in modernen Web-Anwendungen.

Der entscheidende Unterschied zu statischen Imports ist nicht nur syntaktisch: import() kann in Bedingungen, Schleifen und Event-Handlern stehen, weil es sich wie ein normaler Funktionsaufruf verhaelt. Ein Bundler wie Vite oder Webpack erkennt import()-Aufrufe beim Build und erzeugt automatisch einen separaten Chunk fuer das importierte Modul, der erst bei tatsaechlichem Aufruf ueber das Netzwerk nachgeladen wird. Genau dieser Mechanismus liegt jedem modernen Code-Splitting-Setup zugrunde, ob im Framework-Router oder im selbst geschriebenen Ladecode.

Wichtig zu verstehen: Dynamic Import ist eine Sprachfunktion, kein Bundler-Feature. Auch ganz ohne Build-Tool funktioniert import() nativ im Browser und in Node.js, dort laedt es das Zielmodul einfach per Netzwerk-Request beziehungsweise Dateisystemzugriff nach. Bundler nutzen diese native Funktion lediglich, um daraus automatisch separate, cachefaehige Chunks zu erzeugen, statt den kompletten Code in ein einziges Bundle zu packen.

2. Grundmuster: Route-basiertes Code-Splitting

Das haeufigste Anwendungsmuster fuer Dynamic Import ist Route-basiertes Code-Splitting: jede Route einer Single-Page-Application laedt ihre Komponenten erst, wenn der Nutzer tatsaechlich dorthin navigiert. Statt dass die gesamte Anwendung in einem riesigen initialen Bundle ausgeliefert wird, bekommt der Nutzer beim ersten Seitenaufruf nur den Code fuer die aktuell sichtbare Route, alle anderen Routen werden per Dynamic Import erst bei Bedarf nachgeladen.

Dieses Muster reduziert die initiale Ladezeit spuerbar, besonders bei Anwendungen mit vielen selten besuchten Bereichen wie Admin-Panels oder Einstellungsseiten. Der Trade-off: beim ersten Navigieren zu einer neuen Route entsteht eine kurze Verzoegerung, waehrend der Chunk geladen wird, weshalb Ladeindikatoren und Preloading-Strategien, dazu spaeter mehr, in der Praxis fast immer dazugehoeren.


// router.js — minimal route-based code splitting without a framework
const routes = {
  "/dashboard": () => import("./pages/dashboard.js"),
  "/settings": () => import("./pages/settings.js"),
  "/reports": () => import("./pages/reports.js"),
};

async function navigate(path) {
  const loadPage = routes[path];
  if (!loadPage) return renderNotFound();

  showLoadingIndicator();
  try {
    // Dynamic import returns a promise resolving to the module namespace
    const { default: PageComponent } = await loadPage();
    renderPage(new PageComponent());
  } finally {
    hideLoadingIndicator();
  }
}

window.addEventListener("popstate", () => navigate(location.pathname));
navigate(location.pathname);

In Framework-Routern wie React Router oder Vue Router ist genau dieses Muster bereits eingebaut, meist ueber eine lazy()-Hilfsfunktion, die intern nichts anderes tut als import() aufzurufen und das Ergebnis in eine vom Framework erwartete Struktur zu verpacken. Wer versteht, wie Dynamic Import unter der Haube funktioniert, kann Framework-Abstraktionen gezielter einsetzen und bei Bedarf eigene Ladefunktionen mit zusaetzlicher Logik ergaenzen.

3. Feature Detection und bedingtes Laden

Ein weniger bekanntes, aber sehr nuetzliches Muster ist Dynamic Import in Kombination mit Feature Detection: Code fuer selten genutzte Browser-APIs oder Polyfills wird nur dann geladen, wenn er tatsaechlich gebraucht wird. Statt ein Polyfill fuer ResizeObserver immer auszuliefern, prueft man zur Laufzeit, ob die native API vorhanden ist, und laedt das Polyfill nur im negativen Fall per Dynamic Import nach.

Dasselbe Muster funktioniert fuer Nutzer-Einstellungen und A/B-Tests: eine experimentelle Funktion wird nur fuer Nutzer in einer bestimmten Testgruppe per Dynamic Import geladen, alle anderen Nutzer bekommen den zusaetzlichen Code gar nicht erst uebertragen. Das reduziert nicht nur die Bundle-Groesse fuer die Mehrheit der Nutzer, sondern haelt experimentellen Code auch klar vom Kernpfad der Anwendung getrennt.


// Conditional polyfill loading via dynamic import
async function ensureResizeObserver() {
  if (typeof ResizeObserver !== "undefined") return;
  // Only fetched over the network for browsers that actually lack it
  const { default: ResizeObserverPolyfill } = await import(
    "resize-observer-polyfill"
  );
  window.ResizeObserver = ResizeObserverPolyfill;
}

// Feature flag driven experimental module loading
async function loadCheckoutFlow(userSegment) {
  if (userSegment === "experiment-b") {
    const { CheckoutFlowV2 } = await import("./checkout/flow-v2.js");
    return new CheckoutFlowV2();
  }
  const { CheckoutFlowV1 } = await import("./checkout/flow-v1.js");
  return new CheckoutFlowV1();
}

4. Retry-Pattern bei fehlgeschlagenen Chunk-Loads

Ein Problem, das in Produktion regelmaessig auftritt: ein Chunk, der per Dynamic Import nachgeladen wird, existiert nicht mehr, weil zwischenzeitlich ein neues Deployment stattgefunden hat und die alten Chunk-Dateien vom Server oder CDN entfernt wurden. Nutzer, die eine Anwendung laenger geoeffnet haben und dann zu einer noch nicht geladenen Route navigieren, bekommen einen Netzwerkfehler statt der erwarteten Seite, ein Problem, das ohne Retry-Logik zu einer schlechten Nutzererfahrung fuehrt.

Das robuste Dynamic Import Retry-Pattern kombiniert einen Wiederholungsversuch mit exponentiellem Backoff und, falls alle Versuche scheitern, einem erzwungenen Seiten-Reload, das den Nutzer automatisch auf die neueste Deployment-Version bringt. Dieses Muster ist in praktisch jeder produktionsreifen Single-Page-Application notwendig, wird aber erstaunlich haeufig vergessen, bis der erste Nutzer-Bugreport eintrifft.


// Retry helper for dynamic import with exponential backoff
async function importWithRetry(importFn, retries = 3, delayMs = 500) {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      return await importFn();
    } catch (err) {
      const isLastAttempt = attempt === retries - 1;
      if (isLastAttempt) {
        // Chunk likely stale after a new deployment — force a fresh reload
        console.error("Chunk load failed after retries, reloading page", err);
        window.location.reload();
        throw err;
      }
      await new Promise((resolve) => setTimeout(resolve, delayMs * 2 ** attempt));
    }
  }
}

// Usage in a router
const dashboardModule = await importWithRetry(() => import("./pages/dashboard.js"));

Manche Bundler bieten dafuer bereits eingebaute Loesungen, etwa Vite mit dem Event vite:preloadError, das genau in diesem Fall ausgeloest wird und sich zentral abfangen laesst, statt an jeder einzelnen Dynamic Import-Stelle Retry-Logik zu duplizieren. Wer ein solches zentrales Error-Handling einrichtet, muss das Retry-Pattern nicht in jeder Komponente erneut implementieren.

5. Preloading und Prefetching Strategien

Um die Verzoegerung beim ersten Aufruf eines per Dynamic Import geladenen Chunks zu reduzieren, lohnt sich Preloading: der Chunk wird bereits geladen, bevor der Nutzer die entsprechende Aktion tatsaechlich ausloest, meist ausgeloest durch ein Hover-Event auf einem Link oder durch Sichtbarkeit im Viewport ueber den Intersection Observer. Der Trick dabei ist, den Dynamic Import Aufruf fruehzeitig zu starten, das Ergebnis aber erst bei tatsaechlicher Navigation zu verwenden.

Der Browser cached das Modul intern, sodass ein zweiter import()-Aufruf fuer dieselbe URL kein erneutes Netzwerk-Request ausloest, sondern das bereits aufgeloeste Promise zurueckgibt. Genau diese Eigenschaft macht Preloading effektiv: das fruehe import() beim Hover startet den Download, das spaetere import() beim Klick nutzt lediglich das Ergebnis desselben Downloads weiter.


// Preload on hover, use the cached result on click
const preloadedModules = new Map();

function preloadRoute(path) {
  if (preloadedModules.has(path)) return;
  // Kicks off the network request early; result is memoized by the module cache
  preloadedModules.set(path, import(`./pages/${path}.js`));
}

document.querySelectorAll("a[data-route]").forEach((link) => {
  link.addEventListener("mouseenter", () => preloadRoute(link.dataset.route), {
    once: true,
  });
});

async function navigateWithPreload(path) {
  const modulePromise = preloadedModules.get(path) ?? import(`./pages/${path}.js`);
  const { default: PageComponent } = await modulePromise;
  renderPage(new PageComponent());
}

Zusaetzlich unterstuetzen moderne Bundler statische Hinweise wie /* webpackPrefetch: true */ oder Vite-Aequivalente direkt im Dynamic Import Aufruf, die den Browser anweisen, den Chunk mit niedriger Prioritaet waehrend Leerlaufzeiten vorzuladen, ganz ohne dass eigener Preloading-Code geschrieben werden muss.

6. Dynamic Import mit Template-Strings: Grenzen

Ein haeufig gesehenes, aber problematisches Muster ist Dynamic Import mit vollstaendig dynamisch berechneten Pfaden, etwa import(getModulePath()). Waehrend Node.js und der Browser das zur Laufzeit grundsaetzlich unterstuetzen, koennen Bundler solche Pfade beim Build nicht statisch analysieren und muessen daher potenziell alle infrage kommenden Dateien in den Build einschliessen, was das Ziel des Code-Splittings komplett untergraebt.

Die Loesung ist ein Template-String mit statischem Praefix und Suffix, bei dem nur ein Teil dynamisch ist, etwa import(\`./locales/${lang}.json\`). Bundler wie Webpack und Vite koennen dieses Muster statisch erkennen, weil das Verzeichnis und die Dateiendung bekannt sind, und erzeugen automatisch einen Chunk pro moeglicher Datei im passenden Verzeichnis. Vollstaendig freie Variablen als kompletter Importpfad bleiben dagegen fuer den Bundler unanalysierbar und sollten vermieden werden.


// GOOD: bundlers can statically analyze this pattern
async function loadLocale(lang) {
  // Static prefix + suffix, only the middle part is dynamic
  const messages = await import(`./locales/${lang}.json`);
  return messages.default;
}

// PROBLEMATIC: fully dynamic path, bundlers cannot analyze it
async function loadModuleByFullPath(fullPath) {
  return import(fullPath); // may include the entire codebase in the bundle
}

// Safer alternative: an explicit allowlist map
const localeLoaders = {
  de: () => import("./locales/de.json"),
  en: () => import("./locales/en.json"),
  fr: () => import("./locales/fr.json"),
};
async function loadLocaleSafe(lang) {
  const loader = localeLoaders[lang] ?? localeLoaders.en;
  return (await loader()).default;
}

7. Import Attributes fuer JSON und CSS Module

Neben JavaScript-Modulen unterstuetzt Dynamic Import mittlerweile auch Import Attributes, frueher als Import Assertions bekannt, mit denen der erwartete Modultyp explizit angegeben wird. Die Syntax import(url, { with: { type: "json" } }) laedt eine JSON-Datei direkt als Modul, ohne einen separaten fetch-Aufruf und manuelles Parsen zu benoetigen, und der Browser verweigert das Laden, wenn der tatsaechliche MIME-Type nicht zum erwarteten Typ passt, was eine zusaetzliche Sicherheitsebene einzieht.

Fuer CSS Module gilt aehnliches: mit { with: { type: "css" } } laesst sich ein Stylesheet als CSSStyleSheet-Objekt importieren, das sich direkt einem Shadow DOM oder dem Dokument zuweisen laesst. Dieses Muster reduziert Boilerplate-Code fuer Web Components erheblich, weil Style-Definitionen nicht mehr per String-Konkatenation oder separatem Stylesheet-Link eingebunden werden muessen.


// Dynamic import of JSON as a module, with an explicit type attribute
async function loadConfig() {
  const { default: config } = await import("./config.json", {
    with: { type: "json" },
  });
  return config;
}

// Dynamic import of a CSS module for a Web Component
async function attachStyles(shadowRoot) {
  const { default: sheet } = await import("./component.css", {
    with: { type: "css" },
  });
  shadowRoot.adoptedStyleSheets = [sheet];
}

8. Fehlerbehandlung und Error Boundaries

Weil Dynamic Import ein Promise zurueckgibt, laesst sich Fehlerbehandlung ganz natuerlich mit try/catch um ein await oder mit .catch() auf dem zurueckgegebenen Promise umsetzen. In React-Anwendungen kombiniert man lazy()-geladene Komponenten typischerweise mit einer Error Boundary, die einen Ladefehler abfaengt und eine Fallback-UI anzeigt, statt dass die gesamte Anwendung mit einem unbehandelten Fehler abstuerzt.

Ein haeufig uebersehener Aspekt: Netzwerkfehler bei Dynamic Import unterscheiden sich von echten Modulfehlern, etwa Syntaxfehlern im geladenen Code. Fuer Netzwerkfehler ist das Retry-Pattern aus Abschnitt vier die richtige Antwort, fuer echte Fehler im Modul selbst hilft kein erneuter Ladeversuch, dort muss der Fehler geloggt und dem Nutzer eine sinnvolle Fallback-Ansicht angezeigt werden, ohne die Anwendung in eine Retry-Schleife zu schicken.

9. Statischer vs. dynamischer Import im Vergleich

Die Entscheidung zwischen statischem import und Dynamic Import ist keine reine Stilfrage, sondern haengt vom tatsaechlichen Nutzungsmuster des Moduls ab.

Kriterium Statischer Import Dynamic Import Empfehlung
Ladezeitpunkt Beim Modul-Parsing, vor Ausfuehrung Zur Laufzeit, on demand Dynamic fuer selten genutzten Code
Rueckgabewert Direkte Bindings Promise mit Namespace-Objekt await/then noetig bei Dynamic Import
Statische Analyse Vollstaendig moeglich Nur mit statischem Praefix/Suffix Freie Variablen als Pfad vermeiden
Bundle-Einfluss Teil des Haupt-Bundles Eigener, separater Chunk Dynamic fuer kleinere initiale Bundle-Groesse
Bedingtes Laden Nicht moeglich Voll unterstuetzt Dynamic Import fuer Feature Detection

In der Praxis ergaenzen sich beide Formen: der Kernpfad der Anwendung, der bei jedem Seitenaufruf gebraucht wird, bleibt statisch importiert fuer maximale Performance beim ersten Laden, waehrend selten genutzte Routen, grosse Bibliotheken und bedingter Code ueber Dynamic Import gezielt nachgeladen werden.

Mironsoft

Bundle-Optimierung und Ladezeiten-Tuning

Riesiges Initial-Bundle statt schnellem First Load?

Wir analysieren eure Bundle-Struktur, richten Route-basiertes Code-Splitting mit Retry- und Preloading-Logik ein und reduzieren die initiale Ladezeit eurer Anwendung messbar.

Bundle-Analyse

Identifikation von Code, der sich fuer Dynamic Import eignet

Code-Splitting-Setup

Route-basiertes Splitting mit robustem Retry-Pattern fuer Deployments

Performance-Monitoring

Messung des tatsaechlichen Effekts auf Ladezeit und Core Web Vitals

10. Zusammenfassung

Dynamic Import ist die Grundlage jedes Code-Splitting-Setups in modernen Web-Anwendungen: der Funktionsaufruf import() gibt ein Promise zurueck, laesst sich in Bedingungen und Event-Handlern verwenden und ermoeglicht Route-basiertes Laden, Feature Detection und bedingten Code fuer A/B-Tests. Robuste Anwendungen ergaenzen Dynamic Import immer um ein Retry-Pattern fuer fehlgeschlagene Chunk-Loads nach neuen Deployments, sonst entstehen unnoetige Fehlerbilder fuer Nutzer mit laenger geoeffneten Tabs.

Preloading bei Hover oder Sichtbarkeit reduziert die wahrgenommene Ladezeit spuerbar, waehrend Template-Strings mit statischem Praefix und Suffix dafuer sorgen, dass Bundler Dynamic Import Aufrufe weiterhin statisch analysieren koennen. Import Attributes erweitern Dynamic Import zusaetzlich auf JSON- und CSS-Module, ganz ohne manuelles Parsen. Wer diese Muster konsequent anwendet, bekommt kleinere initiale Bundles ohne Abstriche bei Robustheit.

Dynamic Import — Das Wichtigste auf einen Blick

Grundprinzip

import() gibt ein Promise zurueck und laedt Module zur Laufzeit, nutzbar in Bedingungen und Event-Handlern.

Retry-Pattern

Exponentielles Backoff plus erzwungener Reload bei dauerhaftem Fehlschlag verhindert defekte Chunk-Ladevorgaenge nach Deployments.

Preloading

Fruehzeitiger import()-Aufruf bei Hover oder Sichtbarkeit nutzt das native Modul-Caching des Browsers aus.

Analysierbarkeit

Template-Strings mit statischem Praefix und Suffix bleiben fuer Bundler analysierbar, vollstaendig freie Pfade nicht.

11. FAQ: Dynamic Import

1Was gibt Dynamic Import zurueck?
Ein Promise, das mit dem Modul-Namespace-Objekt aufgeloest wird, Named und Default Exports werden darueber zugaenglich.
2In Bedingungen nutzbar?
Ja, weil import() ein normaler Funktionsaufruf ist, im Gegensatz zum statischen import-Statement.
3Warum schlaegt es nach Deployments fehl?
Alte Chunk-Dateien werden vom Server entfernt, waehrend Nutzer noch die alte Version geoeffnet haben. Retry mit Fallback-Reload loest das.
4Preloading vs. Prefetching?
Preloading mit hoher Prioritaet fuer bald benoetigten Code, Prefetching mit niedriger Prioritaet fuer spaeter benoetigten Code.
5Warum nicht jeder Pfad analysierbar?
Vollstaendig dynamisch berechnete Pfade lassen sich beim Build nicht auswerten, der Bundler muesste alle infrage kommenden Dateien einschliessen.
6Was sind Import Attributes?
Geben den erwarteten Modultyp explizit an, etwa JSON oder CSS, der Browser prueft den MIME-Type vor dem Laden.
7Unterschied zu React lazy()?
lazy() ist ein Wrapper um import(), der das Ergebnis fuer Suspense und Fehlerbehandlung in React aufbereitet.
8Erneuter Download bei mehrfachem Aufruf?
Nein, der Browser cached das Modul intern, ein zweiter Aufruf fuer dieselbe URL nutzt das vorhandene Ergebnis.
9Jeden Import dynamisch machen?
Nein, Kernfunktionalitaet bleibt statisch, Dynamic Import lohnt sich fuer selten genutzten oder bedingten Code.
10Funktioniert es in Node.js?
Ja, seit Node 14 laedt import() sowohl ESM als auch CJS, unabhaengig vom Modulsystem der aufrufenden Datei.