Module Federation in JavaScript
Overview
Module Federation is a Webpack 5 feature that lets you split a JavaScript application into separate bundles that share code at runtime. One bundle can load modules from another bundle on demand, without the two having to be built together. Dependencies that both bundles use — like React or Lodash — can be shared automatically so they only load once.
Before Module Federation, sharing code between separate applications meant bundling everything together or using iframe-based micro-frontends. Module Federation gives you a runtime alternative: applications reference each other’s modules as if they were local, and Webpack handles the loading.
Host and Remote
Module Federation works with two roles:
- Remote: an application that exposes some of its modules to other applications
- Host: an application that consumes those exposed modules at runtime
The same application can be both a host and a remote — it exposes its own components while consuming modules from other applications.
Basic Setup
Exposing modules from the remote
In your webpack config, use ModuleFederationPlugin to expose components:
// webpack.config.js (Remote App)
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
mode: 'development',
devServer: {
port: 3001,
},
output: {
publicPath: 'http://localhost:3001/',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
'./Header': './src/Header',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
remoteEntry.js is the file the host loads to access the remote’s modules.
Consuming remote modules in the host
// webpack.config.js (Host App)
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
mode: 'development',
devServer: {
port: 3000,
},
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remote: 'remote@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
Loading a remote component
In the host application, import and use the remote component as if it were local:
import React, { Suspense } from 'react';
const Button = React.lazy(() => import('remote/Button'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Button onClick={() => console.log('clicked')}>Click me</Button>
</Suspense>
);
}
The import string 'remote/Button' tells Webpack to load the Button module from the remote application at runtime. The Suspense boundary handles the async loading.
How the Loading Works
When the host builds, it does not bundle the remote’s code. Instead, Webpack records the remote’s location in the bundle. At runtime, when the host needs remote/Button:
- The host’s runtime loads
remoteEntry.jsfrom the remote - The host asks the container for
remote/Button - The remote’s container serves the module
- If the module uses shared dependencies (like
react), the host supplies them from its own copy
This is why shared: { react: { singleton: true } } matters — it prevents two copies of React from loading. Without singleton, each app would load its own copy.
Shared Dependencies
The shared option controls which dependencies are shared between host and remote:
new ModuleFederationPlugin({
name: 'host',
remotes: {
remote: 'remote@http://localhost:3001/remoteEntry.js',
},
shared: {
// Singleton: only one copy loads, regardless of which app loads first
react: { singleton: true },
'react-dom': { singleton: true },
// Non-singleton: each app gets its own copy
lodash: { singleton: false },
// With version constraints: use this version if available, fall back otherwise
axios: { requiredVersion: '^1.0.0' },
},
});
The singleton behavior is particularly important for React — two copies of the reconciler would cause the DOM to behave incorrectly.
Dynamic Remote Loading
Beyond the build-time import syntax, you can load remote modules dynamically at runtime. This is useful when you do not know which remote to load until the app is running:
// Dynamic remote loader
async function loadRemote(url, scope, module) {
await __webpack_init_sharing__(scope);
const container = await import(/* webpackIgnore: true */ url);
await container.init(__webpack_share_scopes__[scope]);
const factory = await container.get(module);
return factory();
}
// Load a module from a remote at runtime
const Button = await loadRemote(
'http://localhost:3001/remoteEntry.js',
'default',
'./Button'
);
Button.onClick(); // runs the button's click handler
This pattern lets you configure remote URLs from a server, enabling truly dynamic micro-frontend routing.
Module Federation 2.0
Module Federation has evolved beyond Webpack. The Module Federation 2.0 specification defines a build-tool-agnostic standard for federated modules, now implemented in:
- Rspack (Rust-based Webpack-compatible bundler)
- Farm (Rust-based bundler)
- Vite (via plugins)
The core concepts remain the same, but the underlying loading mechanism no longer depends on Webpack’s runtime.
Common Use Cases
Micro-frontends
Module Federation is most commonly used in micro-frontend architectures. Each team builds and deploys their application independently. The shell (host) application loads the team’s components at runtime:
// Shell app loads Header and Footer from different remotes
const Header = React.lazy(() => import('teamHeader/Header'));
const Footer = React.lazy(() => import('teamFooter/Footer'));
Shared component library
A design system or component library can be deployed as a remote. Applications consume it without bundling it:
// Host app uses a shared component library
import { Button, Modal, Dropdown } from 'designSystem';
Plugin systems
If your application supports plugins, Module Federation provides a standard way for plugins to expose functionality to the host and consume shared services:
// Plugin remote
exposes: {
'./plugin': './src/plugin',
},
shared: {
hostApi: { import: 'hostApi', shareScope: 'plugin' },
}
Gotchas
Singleton version mismatches. If the host and remote specify incompatible versions of a shared singleton, Module Federation picks the higher version — but the behavior may be unpredictable. Align shared dependency versions across applications.
Build output must be deployed consistently. The host’s publicPath must be configured correctly so that remoteEntry.js and the chunk files are accessible at the expected URLs.
Development vs production. In development with devServer, hot module reloading works across host and remote, but the setup is more fragile than in production. Check that publicPath is set correctly in both modes.
Circular shared dependencies. If remote A shares a module that depends on remote B, and remote B shares a module that depends on A, you can get initialization order problems. Avoid circular dependencies in shared modules.
See Also
- /guides/javascript-modules-esm/ — JavaScript module system (ESM and CommonJS) that Module Federation builds on
- /guides/javascript-pwa-guide/ — related architecture topic: loading application fragments at runtime
- /guides/javascript-web-workers/ — another way to load code in isolated contexts