Module Federation Explained: Sharing Code at Runtime
Module Federation lets separately built and deployed JavaScript apps load each other's modules at runtime. How hosts, remotes, and shared dependencies work.
Module Federation is a bundler feature that lets one JavaScript application load modules from another, separately built and separately deployed application at runtime. Instead of publishing shared code to a package registry and rebuilding every consumer when it changes, a team exposes modules from its own deployment, and other apps fetch them in the browser when needed. It first shipped as a core feature of webpack 5 and has since been adopted by other bundlers through native support or plugins.
It is the most common technical foundation for micro-frontends, where several teams own different parts of one user-facing product.
The core vocabulary
Module Federation has three concepts worth getting straight before looking at any configuration.
- Remote — an application that exposes modules for others to consume. A checkout team might expose a
CartWidgetcomponent. - Host — an application that consumes modules from one or more remotes. The main shell of a storefront might load the cart widget from the checkout team.
- Shared — dependencies that hosts and remotes agree to load only once, such as React or a design-system library, so the page does not ship two copies.
An application can be both a host and a remote. The roles describe a relationship, not a type of app.
How it works at runtime
When a remote is built, the bundler emits a small manifest script, conventionally named remoteEntry.js. It is not the remote’s code; it is a lookup table that says which modules are exposed, where their chunks live, and which shared dependencies they need at which versions.
When the host needs a remote module, roughly this happens:
- The host loads the remote’s entry script from the remote’s URL.
- The host and remote negotiate shared dependencies. If the host already loaded a compatible version of React, the remote uses it instead of downloading its own.
- The host requests the exposed module. The remote’s container fetches the relevant chunks and returns the module.
- The host renders or calls it like any other module loaded through a dynamic import.
Because the remote is fetched at runtime, the checkout team can deploy a new version of the widget and every host picks it up on the next page load, without a host rebuild.
A minimal configuration
Here is the shape of a webpack configuration for the remote:
// checkout/webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
const deps = require("./package.json").dependencies;
module.exports = {
// ...entry, output, loaders
plugins: [
new ModuleFederationPlugin({
name: "checkout",
filename: "remoteEntry.js",
exposes: {
"./CartWidget": "./src/CartWidget",
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
"react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
},
}),
],
};
And the host:
// shell/webpack.config.js
new ModuleFederationPlugin({
name: "shell",
remotes: {
checkout: "checkout@https://checkout.example.com/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
"react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
},
});
The host then imports the remote module by its federated name:
const CartWidget = React.lazy(() => import("checkout/CartWidget"));
Wrapping it in React.lazy and a Suspense boundary keeps the remote off the critical path and gives you a place to show a fallback while it loads.
The bootstrap pattern
A common first error is “Shared module is not available for eager consumption.” It happens because the host’s entry file imports React synchronously before the federation runtime has had a chance to negotiate which copy of React to use.
The standard fix is an asynchronous boundary. Move the real application startup into a separate file and load it dynamically:
// index.js
import("./bootstrap");
// bootstrap.js
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(<App />);
That one dynamic import gives the runtime a moment to initialize shared scopes before anything consumes them. The eager: true option on a shared dependency is the alternative, but it bundles that dependency into the initial chunk, which undoes part of the point.
Shared dependency options
The shared block is where most of the real design decisions live:
singleton— only one copy may exist on the page. Required for libraries that keep global state or rely on identity, such as React and most state-management libraries.requiredVersion— the semver range this app needs. If the loaded copy does not satisfy it, the runtime warns, or falls back to loading the app’s own copy when singleton is not set.strictVersion— turn a version mismatch into a hard error instead of a warning.eager— include the dependency in the initial bundle rather than loading it asynchronously.
Getting these wrong is the most common source of subtle bugs, such as two React copies on one page breaking hooks.
Module Federation vs other ways to share code
| npm packages | Import maps | Module Federation | |
|---|---|---|---|
| When code is resolved | Build time | Runtime, by the browser | Runtime, by the bundler’s runtime |
| Deploy independently | No, consumers rebuild | Yes | Yes |
| Shared dependency negotiation | Package manager dedupes at install | Manual, one URL per specifier | Built in, with version ranges |
| Tooling requirement | Any bundler | Native browser feature | Bundler with federation support |
| Best for | Stable libraries | Unbundled or lightly bundled apps | Multi-team apps with bundled builds |
Import maps solve a related problem natively in the browser, mapping bare specifiers to URLs. Module Federation adds version negotiation and chunk management on top of a bundled build, which is why the two are sometimes combined.
Trade-offs to plan for
Runtime composition moves failure from build time to the browser. If a remote is down or ships a breaking change, the host finds out in production. Treat exposed modules as public APIs: version them, test the contract, and wrap remote components in error boundaries.
Other recurring costs:
- Types. TypeScript cannot see a module that only exists at runtime. Teams generate and publish type declarations for exposed modules or use tooling that syncs them.
- Loading waterfalls. A host that loads a remote entry, then a chunk, then that chunk’s own dependencies adds round trips. Preloading entry scripts helps, as does keeping exposed modules coarse.
- Styling collisions. Separately built apps can ship conflicting global CSS. Scoped styles or a shared design system avoid surprises.
- Organizational fit. If one team owns everything in a single repository, regular code splitting gives you most of the benefits with none of the coordination overhead. The monorepo vs polyrepo choice often decides whether federation is worth it.
The takeaway
Module Federation lets independently deployed apps share modules at runtime: remotes expose code through a small entry manifest, hosts load it on demand, and both negotiate shared dependencies so the page loads one copy of each. It is a strong fit for multi-team frontends with separate release cycles. Use singletons for stateful libraries, add an async bootstrap boundary, treat exposed modules as versioned APIs, and reach for plain code splitting when one team owns the whole app.
Tagged
Keep reading
Takina · · 5 min read JavaScript == vs ===: Loose vs Strict Equality Explained
JavaScript's == converts types before comparing; === does not. How coercion works, the classic gotchas, where Object.is fits, and which to use.
Takina · · 5 min read JavaScript Strings and Unicode: Code Units vs Graphemes
JavaScript string length counts UTF-16 code units, not characters. How code units, code points, and grapheme clusters differ and how to handle each.
Takina · · 5 min read Event Bubbling vs Capturing in JavaScript, Explained
Event bubbling and capturing are the two directions a DOM event travels. How propagation phases work, when to use each, and how to stop them.