Build Tools — Module Federation
Webpack Module Federation (introduced in Webpack 5) is a JavaScript architecture that allows a running application to dynamically load code from another independently deployed application at runtime. It enables true micro-frontend architectures where teams develop, deploy, and version their applications independently while still composing them into a seamless user experience.
Unlike traditional micro-frontend approaches (iframe, server-side composition, build-time integration), Module Federation operates at the module bundler level. Applications share modules directly in the browser with no runtime overhead beyond the actual module loading. This makes it the most performant and flexible micro-frontend solution available today.
Module Federation allows a Webpack build to specify which modules it exposes to other applications and which modules it consumes from other applications at runtime. This creates a distributed module graph where applications share dependencies and components dynamically.
- Exposes — Modules that your application makes available to other federated applications.
- Remotes — External applications whose exposed modules your application consumes.
- Shared — Dependencies that should be shared across federated applications (like React) to avoid duplicate bundles.
- Host — The application that consumes remote modules and orchestrates the federated experience.
info
Module Federation is built on three core concepts: exposes determine what your application shares, remotes define what other applications you consume, and shared controls how common dependencies are deduplicated across federated boundaries.
| 1 | // Module federation plugin configuration |
| 2 | const { ModuleFederationPlugin } = require("webpack").container; |
| 3 | |
| 4 | new ModuleFederationPlugin({ |
| 5 | // Name of this federated application |
| 6 | name: "shell", |
| 7 | |
| 8 | // Modules this application exposes to others |
| 9 | exposes: { |
| 10 | "./Header": "./src/components/Header", |
| 11 | "./Footer": "./src/components/Footer", |
| 12 | "./AuthProvider": "./src/auth/AuthProvider", |
| 13 | "./Navigation": "./src/components/Navigation", |
| 14 | }, |
| 15 | |
| 16 | // Remote applications this application consumes |
| 17 | remotes: { |
| 18 | dashboard: "dashboard@http://localhost:3001/remoteEntry.js", |
| 19 | analytics: "analytics@http://localhost:3002/remoteEntry.js", |
| 20 | settings: "settings@http://localhost:3003/remoteEntry.js", |
| 21 | }, |
| 22 | |
| 23 | // Dependencies shared across federated modules |
| 24 | shared: { |
| 25 | react: { |
| 26 | singleton: true, // Only one React instance |
| 27 | requiredVersion: "^18.0.0", |
| 28 | eager: true, // Load React synchronously |
| 29 | }, |
| 30 | "react-dom": { |
| 31 | singleton: true, |
| 32 | requiredVersion: "^18.0.0", |
| 33 | eager: true, |
| 34 | }, |
| 35 | "react-router-dom": { |
| 36 | singleton: true, |
| 37 | requiredVersion: "^6.0.0", |
| 38 | }, |
| 39 | }, |
| 40 | }); |
warning
A complete Module Federation setup involves at least two applications: a host that provides the shell and one or more remotes that provide features. Each remote application needs its own Webpack configuration with the federation plugin.
| 1 | // Remote application (dashboard) configuration |
| 2 | // webpack.config.js — dashboard (remote) |
| 3 | const { ModuleFederationPlugin } = require("webpack").container; |
| 4 | const deps = require("./package.json").dependencies; |
| 5 | |
| 6 | module.exports = { |
| 7 | // Must use unique port for each remote |
| 8 | devServer: { port: 3001 }, |
| 9 | |
| 10 | plugins: [ |
| 11 | new ModuleFederationPlugin({ |
| 12 | name: "dashboard", |
| 13 | filename: "remoteEntry.js", // Entry point for host to load |
| 14 | exposes: { |
| 15 | "./DashboardWidget": "./src/DashboardWidget", |
| 16 | "./MetricsGrid": "./src/MetricsGrid", |
| 17 | "./ChartWidget": "./src/ChartWidget", |
| 18 | }, |
| 19 | shared: { |
| 20 | react: { |
| 21 | singleton: true, |
| 22 | requiredVersion: deps.react, |
| 23 | }, |
| 24 | "react-dom": { |
| 25 | singleton: true, |
| 26 | requiredVersion: deps["react-dom"], |
| 27 | }, |
| 28 | "recharts": { |
| 29 | singleton: true, |
| 30 | requiredVersion: deps.recharts, |
| 31 | }, |
| 32 | }, |
| 33 | }), |
| 34 | ], |
| 35 | }; |
| 36 | |
| 37 | // Host application — consuming remote modules |
| 38 | // webpack.config.js — shell (host) |
| 39 | const { ModuleFederationPlugin } = require("webpack").container; |
| 40 | |
| 41 | module.exports = { |
| 42 | devServer: { port: 3000 }, |
| 43 | |
| 44 | plugins: [ |
| 45 | new ModuleFederationPlugin({ |
| 46 | name: "shell", |
| 47 | remotes: { |
| 48 | dashboard: "dashboard@http://localhost:3001/remoteEntry.js", |
| 49 | analytics: "analytics@http://localhost:3002/remoteEntry.js", |
| 50 | }, |
| 51 | shared: { |
| 52 | react: { singleton: true, requiredVersion: "^18.0.0" }, |
| 53 | "react-dom": { singleton: true, requiredVersion: "^18.0.0" }, |
| 54 | }, |
| 55 | }), |
| 56 | ], |
| 57 | }; |
Module Federation supports several architectural patterns for composing micro-frontends. Each pattern has tradeoffs in terms of coupling, deployment independence, and user experience.
| Pattern | Description | Best For |
|---|---|---|
| Host + Remotes | Single shell app loads feature modules from external remotes | Enterprise portals, admin panels |
| Dynamic Remotes | Remotes loaded dynamically based on runtime configuration | Multi-tenant apps, white-label solutions |
| Self-Healing | Each remote can operate as a standalone app if host is unavailable | Mission-critical features, offline-capable apps |
| Nested Federation | A remote itself hosts other federated modules | Large organizations with nested teams |
| Islands Architecture | Independent federated modules with minimal host orchestration | Content-heavy sites, marketing pages |
warning
Consuming federated modules at runtime is straightforward. The remote application loads its remoteEntry.js script, which registers the exposed modules with the Webpack runtime. The host then uses dynamic imports to load specific modules from the remote.
| 1 | // Host application — consuming remote modules at runtime |
| 2 | import React, { Suspense, lazy } from "react"; |
| 3 | import ErrorBoundary from "./ErrorBoundary"; |
| 4 | |
| 5 | // Lazy load the remote DashboardWidget component |
| 6 | const DashboardWidget = lazy(() => |
| 7 | import("dashboard/DashboardWidget") |
| 8 | ); |
| 9 | |
| 10 | // Lazy load remote Analytics components |
| 11 | const MetricsGrid = lazy(() => |
| 12 | import("analytics/MetricsGrid") |
| 13 | ); |
| 14 | |
| 15 | // Render with fallback and error boundaries |
| 16 | function DashboardPage() { |
| 17 | return ( |
| 18 | <div> |
| 19 | <h1>Dashboard</h1> |
| 20 | <ErrorBoundary fallback={<div>Widget failed to load</div>}> |
| 21 | <Suspense fallback={<div className="loading-pulse">Loading...</div>}> |
| 22 | <DashboardWidget userId={user.id} /> |
| 23 | </Suspense> |
| 24 | </ErrorBoundary> |
| 25 | |
| 26 | <ErrorBoundary fallback={<div>Metrics unavailable</div>}> |
| 27 | <Suspense fallback={<div className="loading-pulse">Loading metrics...</div>}> |
| 28 | <MetricsGrid dateRange={selectedRange} /> |
| 29 | </Suspense> |
| 30 | </ErrorBoundary> |
| 31 | </div> |
| 32 | ); |
| 33 | } |
| 34 | |
| 35 | // Dynamic remote loading — remotes decided at runtime |
| 36 | async function loadRemoteModule(remoteName, modulePath) { |
| 37 | // Dynamically load remoteEntry.js from a URL determined at runtime |
| 38 | const container = await import(/* webpackIgnore: true */ remoteName); |
| 39 | const factory = await container.get(modulePath); |
| 40 | return factory(); |
| 41 | } |
| 42 | |
| 43 | // Usage: load any remote module by URL |
| 44 | const RemoteComponent = React.lazy(() => |
| 45 | loadRemoteModule( |
| 46 | "https://feature-flags.example.com/remoteEntry.js", |
| 47 | "./FeatureFlag" |
| 48 | ) |
| 49 | ); |
React is the most common framework used with Module Federation. The key considerations are shared React instance, routing coordination, and state management across federated boundaries.
| 1 | // Shared routing with React Router |
| 2 | // Host application — provide router to federated modules |
| 3 | import { BrowserRouter, Routes, Route } from "react-router-dom"; |
| 4 | import React, { Suspense, lazy } from "react"; |
| 5 | |
| 6 | const DashboardWidget = lazy(() => import("dashboard/DashboardWidget")); |
| 7 | const SettingsWidget = lazy(() => import("settings/SettingsWidget")); |
| 8 | |
| 9 | function AppShell() { |
| 10 | return ( |
| 11 | <BrowserRouter> |
| 12 | <AppShell> |
| 13 | <Routes> |
| 14 | <Route path="/" element={<Home />} /> |
| 15 | <Route |
| 16 | path="/dashboard/*" |
| 17 | element={ |
| 18 | <Suspense fallback={<Loading />}> |
| 19 | <DashboardWidget /> |
| 20 | </Suspense> |
| 21 | } |
| 22 | /> |
| 23 | <Route |
| 24 | path="/settings/*" |
| 25 | element={ |
| 26 | <Suspense fallback={<Loading />}> |
| 27 | <SettingsWidget /> |
| 28 | </Suspense> |
| 29 | } |
| 30 | /> |
| 31 | </Routes> |
| 32 | </AppShell> |
| 33 | </BrowserRouter> |
| 34 | ); |
| 35 | } |
| 36 | |
| 37 | // Remote application — handle routing within its scope |
| 38 | // The remote receives the host's router context |
| 39 | function DashboardWidget() { |
| 40 | // Use Routes inside the remote — it inherits the host's router |
| 41 | return ( |
| 42 | <Routes> |
| 43 | <Route index element={<DashboardHome />} /> |
| 44 | <Route path="analytics" element={<Analytics />} /> |
| 45 | <Route path="reports" element={<Reports />} /> |
| 46 | </Routes> |
| 47 | ); |
| 48 | } |
info
Angular works with Module Federation through the @angular-architects/module-federation package, which provides Angular-specific tooling and schematics for generating the required configuration.
| 1 | // Angular Module Federation configuration |
| 2 | // angular.json — custom webpack builder |
| 3 | { |
| 4 | "projects": { |
| 5 | "shell": { |
| 6 | "architect": { |
| 7 | "build": { |
| 8 | "builder": "@angular-architects/module-federation:webpack-browser" |
| 9 | } |
| 10 | } |
| 11 | } |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | // webpack.config.js — Angular federation |
| 16 | const { ModuleFederationPlugin } = require("webpack").container; |
| 17 | const { shareAll } = require("@angular-architects/module-federation"); |
| 18 | |
| 19 | module.exports = { |
| 20 | output: { |
| 21 | uniqueName: "mfe1", |
| 22 | publicPath: "auto", |
| 23 | }, |
| 24 | optimization: { |
| 25 | runtimeChunk: false, // Required for Module Federation |
| 26 | }, |
| 27 | plugins: [ |
| 28 | new ModuleFederationPlugin({ |
| 29 | name: "mfe1", |
| 30 | filename: "remoteEntry.js", |
| 31 | exposes: { |
| 32 | "./Module": "./src/app/flights/flights.module.ts", |
| 33 | }, |
| 34 | shared: shareAll({ |
| 35 | singleton: true, |
| 36 | strictVersion: true, |
| 37 | requiredVersion: "auto", |
| 38 | }), |
| 39 | }), |
| 40 | ], |
| 41 | }; |
Module Federation is powerful but not the only approach to micro-frontends. Each alternative has different tradeoffs in terms of simplicity, performance, and team independence.
| Approach | Integration | Performance | Complexity |
|---|---|---|---|
| Module Federation | Runtime (Webpack) | Excellent | High |
| iframe | Runtime (native) | Poor | Low |
| Web Components | Runtime (native) | Good | Medium |
| Single SPA | Runtime (orchestrator) | Good | High |
| Nx / Monorepo | Build-time | Excellent | Medium |
| Server-Side Composition | Request-time | Good | Medium |
info
- Always use singletons for frameworks — React, Vue, Angular must be singletons with pinned versions across all federated applications.
- Set a shared dependency strategy early — Define which libraries are shared and which are isolated before development starts. Changing shared configuration later can cause subtle bugs.
- Use ErrorBoundaries — Every federated module import should be wrapped in an ErrorBoundary. A remote going down should never crash the host.
- Prefer publicPath: "auto" — This ensures script paths are resolved relative to the current URL, making deployments location-independent.
- Test remote failures — Write integration tests that simulate remote application failures to verify your error handling and fallback UI work correctly.
- Monitor remoteEntry.js loads — Track the load time and success rate of remote entry scripts. A slow or failed remote load directly impacts user experience.
| 1 | // Module Federation health monitoring |
| 2 | // Instrument remote entry loading with performance tracking |
| 3 | async function loadFederatedModule(remoteName, modulePath) { |
| 4 | const startTime = performance.now(); |
| 5 | |
| 6 | try { |
| 7 | const container = await loadRemoteEntry(remoteName); |
| 8 | const factory = await container.get(modulePath); |
| 9 | const Module = factory(); |
| 10 | |
| 11 | const loadTime = performance.now() - startTime; |
| 12 | reportMetric("module-federation-load", { |
| 13 | remote: remoteName, |
| 14 | module: modulePath, |
| 15 | loadTime, |
| 16 | success: true, |
| 17 | }); |
| 18 | |
| 19 | return Module; |
| 20 | } catch (error) { |
| 21 | const loadTime = performance.now() - startTime; |
| 22 | reportMetric("module-federation-load", { |
| 23 | remote: remoteName, |
| 24 | module: modulePath, |
| 25 | loadTime, |
| 26 | success: false, |
| 27 | error: error.message, |
| 28 | }); |
| 29 | |
| 30 | // Fallback to a cached or default module |
| 31 | return getFallbackModule(remoteName, modulePath); |
| 32 | } |
| 33 | } |