Module Federation 2.0
Wer eine wachsende React-Anwendung als Monolith betreibt, kämpft irgendwann mit langen Build-Zeiten, Deployment-Abhängigkeiten und Team-Konflikten. Module Federation 2.0 löst diese Probleme durch echte Laufzeit-Komposition: Teams deployen ihre App-Teile unabhängig, und die Shell-App komponiert alles dynamisch zusammen.
Inhaltsverzeichnis
- 1. Was Micro-Frontends wirklich lösen
- 2. Module Federation 2.0 — Konzepte und Neuerungen
- 3. Shell-App aufsetzen: Host-Konfiguration
- 4. Remote-App erstellen und exponieren
- 5. Shared Dependencies: React nur einmal laden
- 6. Typsichere Remotes mit TypeScript
- 7. Routing über Micro-Frontend-Grenzen hinweg
- 8. Fehlerbehandlung und Fallbacks
- 9. Ansätze im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Micro-Frontends wirklich lösen
Das Kernproblem großer Frontend-Projekte ist nicht fehlende Technologie, sondern organisatorische Kopplung. Wenn zehn Teams an derselben React-Codebase arbeiten, wird jede Änderung zur Koordinationsaufgabe. Deployments brauchen alle Beteiligten, ein fehlerhafter PR blockiert alle anderen, und Build-Zeiten explodieren, weil jede CI-Pipeline das gesamte Projekt kompiliert. Micro-Frontends lösen das organisatorische Problem, bevor es ein technisches wird: Jedes Team besitzt seinen Teil der Anwendung vollständig – Code, Tests, Deployment-Pipeline.
Module Federation ist nicht einfach Code-Splitting. Beim klassischen Code-Splitting entscheidet der Build-Prozess, welche Chunks entstehen. Bei Module Federation entscheidet der Laufzeit-Loader, welche Remote-Apps geladen werden — und woher. Das bedeutet: Remote-App-Team und Shell-App-Team können vollständig unabhängig deployen. Die Shell fragt zur Laufzeit nach dem aktuellen Stand der Remote, nicht nach dem Stand zum Build-Zeitpunkt. Das ist der fundamentale Unterschied gegenüber allen Build-Zeit-Ansätzen wie NPM-Packages oder Monorepo-Builds.
2. Module Federation 2.0 — Konzepte und Neuerungen
Module Federation 1.0 kam mit Webpack 5 und ermöglichte erstmals echte Laufzeit-Komposition von JavaScript-Modulen über App-Grenzen hinweg. Version 2.0, veröffentlicht als @module-federation/enhanced, bringt mehrere wichtige Verbesserungen: ein verbessertes Typsystem für Remote-Exports, ein Runtime-Plugin-System, das Verhalten ohne Re-Build anpassen kann, und eine Vite-kompatible Implementierung über @originjs/vite-plugin-federation. Neu ist auch die offizielle Unterstützung für dynamische Remote-Registrierung zur Laufzeit ohne vorherige Konfiguration im Build.
Die Grundkonzepte bleiben: Ein Host (Shell-App) konsumiert Remotes. Ein Remote exponiert Komponenten oder Module über eine remoteEntry.js-Datei. Shared definiert, welche Dependencies (vor allem React und React-DOM) zwischen allen Apps geteilt werden, damit jede nur einmal im Browser läuft. Module Federation 2.0 führt zusätzlich Manifest-Dateien ein, die es der Shell erlauben, verfügbare Remotes zur Laufzeit zu entdecken, ohne statische URLs im Build zu haben. Das vereinfacht dynamische Skalierung erheblich.
3. Shell-App aufsetzen: Host-Konfiguration
Die Shell-App ist der Entry-Point für den Nutzer. Sie lädt die globale Navigation, das Routing und — dynamisch zur Laufzeit — die einzelnen Remote-Apps. Die Webpack-Konfiguration der Shell nutzt ModuleFederationPlugin als Host und definiert alle bekannten Remotes mit ihren URLs. In Produktionsumgebungen kommen diese URLs aus Umgebungsvariablen oder einem Service-Discovery-Endpoint, nicht aus dem Build selbst. Die Shell selbst enthält keinen fachlichen Code der Remote-Teams — sie ist reines Scaffolding.
Ein kritisches Detail: Die Shell muss ihre eigene App asynchron booten, damit das Shared-Dependencies-System des Module-Federation-Runtimes vollständig initialisiert ist, bevor irgendetwas gerendert wird. Der übliche Trick ist ein bootstrap.tsx, das dynamisch importiert wird, während index.ts nur diese eine dynamische Import-Zeile enthält. Wer das vergisst, bekommt zur Laufzeit Fehler, weil React mehrfach geladen wird — einmal von der Shell, einmal von einer Remote.
// webpack.config.ts — Shell-App (Host) configuration
import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
export default {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
// Remote URLs come from environment variables at runtime
remotes: {
catalogApp: `catalogApp@${process.env.CATALOG_URL}/remoteEntry.js`,
checkoutApp: `checkoutApp@${process.env.CHECKOUT_URL}/remoteEntry.js`,
accountApp: `accountApp@${process.env.ACCOUNT_URL}/remoteEntry.js`,
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
'react-router-dom': { singleton: true, requiredVersion: '^6.0.0' },
},
}),
],
};
// src/index.ts — async bootstrap trick (REQUIRED for shared deps)
import('./bootstrap');
// src/bootstrap.tsx — actual app entry
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ShellApp } from './ShellApp';
const root = createRoot(document.getElementById('root')!);
root.render(<ShellApp />);
4. Remote-App erstellen und exponieren
Jede Remote-App ist eine eigenständige React-Anwendung — sie kann lokal entwickelt, getestet und deployed werden, ohne dass die Shell existieren muss. Die Webpack-Konfiguration der Remote definiert, welche Komponenten oder Module sie nach außen freigibt. Das Schlüsselwort ist exposes: Es ist ein Mapping von öffentlichem Namen zu internem Dateipfad. Die Remote-App baut dann eine remoteEntry.js-Datei, die der Host zur Laufzeit laden kann.
Wichtig ist, dass exponierte Komponenten keine Annahmen über den Kontext treffen, in dem sie laufen. Sie sollten keine globalen CSS-Variablen erwarten, die nur in ihrer eigenen App definiert sind, und keinen globalen State aus einer Store-Instanz, die vielleicht nicht existiert. Stattdessen erhalten sie alles was sie brauchen über Props oder über explizit geteilte Dependencies (wie einen gemeinsamen React-Query-Client). Gut definierte Schnittstellen — am besten mit TypeScript und exportierten Props-Typen — sind das Fundament stabiler Micro-Frontend-Integration.
// webpack.config.ts — Catalog Remote-App configuration
import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
export default {
plugins: [
new ModuleFederationPlugin({
name: 'catalogApp',
filename: 'remoteEntry.js',
// Public interface: what the shell can import from this remote
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
'./CategoryNav': './src/components/CategoryNav',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// src/components/ProductList.tsx — exposed component (standalone)
import React from 'react';
interface ProductListProps {
categoryId: string;
onProductSelect: (productId: string) => void;
}
// Clean props interface — no assumptions about parent context
export const ProductList: React.FC<ProductListProps> = ({
categoryId,
onProductSelect,
}) => {
// Component fetches its own data — fully autonomous
return <div className="catalog-product-list">{/* ... */}</div>;
};
export default ProductList;
5. Shared Dependencies: React nur einmal laden
Das größte Risiko bei Micro-Frontends ist das mehrfache Laden von React. Wenn Shell-App und Remote-App unterschiedliche React-Instanzen haben, sind Hook-Aufrufe aus der Remote-App aus Sicht der Shell-App Fremdobjekte — das führt zu dem berüchtigten Fehler "Invalid hook call". Das shared-Feld in der Module-Federation-Konfiguration verhindert das: Es teilt denselben React-Bundle zwischen allen Apps, die ihn deklarieren. singleton: true erzwingt, dass immer nur eine Instanz existiert — auch wenn verschiedene Remotes unterschiedliche Minor-Versionen angeben.
Versionskonflikte sind dabei das häufigste Operationsproblem. Wenn die Shell React 18.2 will und eine Remote nur React 18.1 liefert, entscheidet die Singleton-Konfiguration, welche Version gewinnt. Durch requiredVersion kann man Mindestversionen erzwingen und bekommen bei Verletzung zur Laufzeit eine Warnung in der Browser-Konsole. Für den Produktionsbetrieb empfiehlt sich ein zentrales shared-deps-Paket im Monorepo, das die Versionen für alle Apps vorgibt — so bleibt die Konfiguration konsistent, ohne Copy-Paste zwischen mehreren webpack.config.ts-Dateien.
6. Typsichere Remotes mit TypeScript
Das klassische Problem mit Module Federation und TypeScript: Die Shell importiert eine Komponente aus einer Remote mit import('catalogApp/ProductList'), aber TypeScript kennt den Typ dieser Komponente nicht — es gibt keine node_modules/catalogApp zum Zeitpunkt der Kompilierung der Shell. Module Federation 2.0 löst das mit automatisch generierten Typ-Deklarationsdateien. Der Remote-Build erzeugt eine @mf-types.d.ts, die die Shell-App einbinden kann. Die Typen bleiben damit immer synchron mit dem tatsächlichen Export der Remote — ohne manuelles Schreiben von Deklarationen.
Für Teams, die noch nicht Module Federation 2.0 nutzen, gibt es den bewährten Workaround: Ein gemeinsames @types/remotes-Paket im Monorepo, das alle Remote-Schnittstellen als TypeScript-Declarations enthält. Wenn sich eine Remote-Schnittstelle ändert, wird dieses Paket aktualisiert — die Shell-App bekommt beim nächsten Typ-Check sofort Fehlermeldungen, wenn sie eine veraltete API nutzt. So bleibt die Typsicherheit gewahrt, auch wenn Runtime und Compile-Time getrennte Welten sind.
7. Routing über Micro-Frontend-Grenzen hinweg
Routing ist eine der kompliziertesten Fragen in Micro-Frontend-Architekturen. Das empfohlene Muster mit React Router 6: Die Shell-App besitzt die Top-Level-Routen und rendert für jeden Pfad-Prefix die zuständige Remote-App. Die Remote-App bekommt einen basename-Props, der ihr mitteilt, welchen Pfad-Prefix sie verwaltet. Intern nutzt die Remote-App dann ihren eigenen Router — vollständig unabhängig von der Shell. Navigationen innerhalb der Remote bleiben lokal; Navigationen zu anderen Micro-Frontends gehen über die Shell.
Für Cross-Remote-Navigation gibt es zwei Strategien: Entweder nutzt man das native Browser-Routing mit window.history.pushState, das beide Apps über popstate-Events abhören. Oder man definiert einen gemeinsamen Event-Bus als Shared Dependency — eine einfache EventEmitter-Instanz, über die Remotes Navigationsabsichten kommunizieren, ohne direkt voneinander zu wissen. Letzteres ist expliziter und testbarer, erfordert aber einen definierten Kontrakt zwischen den Teams.
// Shell: lazy-load remote components with Suspense + ErrorBoundary
import React, { Suspense, lazy } from 'react';
import { Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from './components/ErrorBoundary';
// Dynamic imports from remote apps — resolved at runtime
const ProductList = lazy(() => import('catalogApp/ProductList'));
const ProductDetail = lazy(() => import('catalogApp/ProductDetail'));
const Checkout = lazy(() => import('checkoutApp/CheckoutFlow'));
const Account = lazy(() => import('accountApp/AccountDashboard'));
const RemoteFallback = () => (
<div className="remote-loading">Anwendungsbereich wird geladen…</div>
);
export const ShellRouter: React.FC = () => (
<Routes>
<Route
path="/catalog/*"
element={
<ErrorBoundary fallback={<div>Katalog nicht verfügbar</div>}>
<Suspense fallback={<RemoteFallback />}>
<ProductList categoryId="root" onProductSelect={() => {}} />
</Suspense>
</ErrorBoundary>
}
/>
<Route
path="/checkout/*"
element={
<ErrorBoundary fallback={<div>Checkout nicht verfügbar</div>}>
<Suspense fallback={<RemoteFallback />}>
<Checkout />
</Suspense>
</ErrorBoundary>
}
/>
</Routes>
);
8. Fehlerbehandlung und Fallbacks
Ein Micro-Frontend-System muss davon ausgehen, dass einzelne Remotes temporär nicht erreichbar sind. Das ist kein Ausnahmezustand, sondern normaler Betrieb in einem verteilten System. React's ErrorBoundary ist die erste Verteidigungslinie: Wenn eine Remote-Komponente beim Laden oder Rendern wirft, fängt die ErrorBoundary den Fehler und rendert einen Fallback. Ohne ErrorBoundary würde ein Remote-Fehler die gesamte Shell-App abschießen — ein inakzeptabler Ausfall für einen Dienst, der eigentlich nur einen kleinen Teil der App betrifft.
Für das Laden der remoteEntry.js selbst bietet Module Federation 2.0 eine Retry-Logik und die Möglichkeit, alternative URLs zu konfigurieren. In der Praxis empfiehlt sich ein Monitoring auf Remote-Ladezeiten: Wenn eine remoteEntry.js mehr als eine Sekunde zum Laden braucht, ist das ein frühes Warnsignal für Deployment-Probleme beim Remote-Team. Alerting auf diese Metriken verhindert, dass ein Remote-Deployment erst durch Nutzerbeschwerden auffällt.
9. Ansätze im direkten Vergleich
Es gibt mehrere Ansätze, um eine React-Anwendung in unabhängig deploybare Teile aufzuteilen. Module Federation ist der mächtigste, aber auch der komplexeste. Die Wahl hängt von Team-Größe, Deployment-Frequenz und der tolerierten Komplexität ab.
| Ansatz | Deployment | Typsicherheit | Empfehlung |
|---|---|---|---|
| Module Federation 2.0 | Vollständig unabhängig | Automatisch via @mf-types | Ab 3+ Teams, hohe Deploy-Frequenz |
| NPM-Packages (Monorepo) | Build-Zeit-Kopplung | Vollständig | 1–2 Teams, seltene Releases |
| iframe-Komposition | Unabhängig | Keine | Nur für Legacy-Integration |
| Web Components | Unabhängig | Begrenzt | Framework-unabhängige Remotes |
| Single-SPA | Unabhängig | Manuell | Mixed-Framework-Umgebungen |
Module Federation gewinnt eindeutig bei der Deployment-Unabhängigkeit und der Integration in den normalen React-Entwicklungsworkflow — Remote-Komponenten sehen aus wie normale React-Komponenten, nur mit einem anderen Import-Pfad. Der Preis ist die Build-Komplexität: zwei Webpack-Konfigurationen, zwei CI-Pipelines, und die Notwendigkeit, Shared-Dependency-Versionen koordiniert zu pflegen. Für kleine Teams ohne echten Deploymentdruck ist ein Monorepo mit NPM-Packages oft die bessere Wahl.
Mironsoft
React-Architektur, Micro-Frontends und Module Federation
React-App in Micro-Frontends aufteilen?
Wir analysieren eure bestehende React-Architektur, definieren sinnvolle Schnittgrenzen und implementieren Module Federation — mit typsicherer Integration und CI-Pipeline pro Team.
Architektur-Review
Schnittgrenzen definieren, Shared-Dependencies analysieren, Team-Struktur evaluieren
Implementierung
Shell-App, Remote-Apps und Typ-Sharing mit Module Federation 2.0 aufbauen
CI/CD pro Team
Unabhängige Deployment-Pipelines mit Typ-Synchronisation und Integration-Tests
10. Zusammenfassung
React Micro-Frontends mit Module Federation 2.0 lösen das organisatorische Skalierungsproblem großer Frontend-Projekte: Teams deployen unabhängig, Typen bleiben dank automatisch generierter Declarations synchron, und die Shell-App komponiert alles zur Laufzeit ohne Build-Zeit-Kopplung. Die Shell nutzt singleton: true für Shared Dependencies, damit React genau einmal im Browser existiert. Remote-Apps exponieren saubere, Props-basierte Schnittstellen ohne Annahmen über den Kontext. ErrorBoundaries und Suspense-Fallbacks halten die Shell stabil, wenn ein Remote temporär nicht erreichbar ist.
Der Weg zur produktionsreifen Micro-Frontend-Architektur ist iterativ: Zuerst die Shell aufsetzen, eine Remote migrieren, den Deployment-Prozess etablieren. Dann weitere Remotes nach und nach auslagern, wenn das Team den Workflow versteht. Das größte Risiko ist nicht die Technologie, sondern eine unklare Schnittgrenze — ein Remote, der zu viel vom Kontext der Shell erwartet, ist genauso gekoppelt wie ein Monolith, nur schwerer zu debuggen.
Module Federation 2.0 — Das Wichtigste auf einen Blick
Async Bootstrap
index.ts importiert nur bootstrap.tsx dynamisch — Pflicht, damit Shared Dependencies korrekt initialisiert werden, bevor React startet.
Singleton React
singleton: true in allen shared-Konfigurationen — verhindert doppeltes React und "Invalid hook call"-Fehler zur Laufzeit.
ErrorBoundary + Suspense
Jede Remote-Komponente in ErrorBoundary und Suspense wrappen — Remote-Ausfälle dürfen die Shell nie zum Absturz bringen.
Typen synchron halten
Module Federation 2.0 generiert @mf-types.d.ts automatisch. Alternativ: gemeinsames @types/remotes-Paket im Monorepo pflegen.