Spa Shell vs Module Federation
Here's a deep, honest, and up-to-date (2025) comparison between single-spa shell and Module Federation host β the two most used micro frontend orchestration approaches today.
People often ask: βArenβt they basically the same thing now?β
The answer is: No β they do very different things under the hood, even though from 10,000 feet they both solve βload multiple independent frontends in one pageβ.
Detailed Breakdown (What Actually Happens)
1. single-spa Shell (Root Config) β The Real Orchestrator
// This is literally all the shell does
import { registerApplication, start } from "single-spa";
registerApplication({
name: "@org/cart",
app: () => import("https://cart.example.com/app.js"), // any URL
activeWhen: "/cart",
});
registerApplication({
name: "@org/checkout",
app: () => import("https://checkout.example.com/main.js"),
activeWhen: (location) => location.pathname.startsWith("/checkout"),
});
start();
- The shell is pure coordination logic.
- It knows nothing about the internals of the MFEs.
- It can load anything that exports the single-spa lifecycles (
bootstrap,mount,unmount).
2025 evolution: Most new single-spa shells are now pure ESM + import maps, no SystemJS, no parcel bundling β shell can be 10β15 kB.
2. Module Federation Host β The Code-Sharing Machine
// webpack.config.js of the HOST (shell)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "shell",
remotes: {
cart: "cart@https://cart.example.com/remoteEntry.js",
checkout: "checkout@https://checkout.example.com/remoteEntry.js",
},
shared: {
react: { singleton: true, eager: true },
"react-dom": { singleton: true },
},
}),
],
};
Then inside your shell React component:
const Cart = React.lazy(() => import("cart/CartApp"));
const Checkout = React.lazy(() => import("checkout/CheckoutApp"));
// Webpack downloads the remoteEntry.js, reads the manifest, downloads chunks β feels like local import
Here the host is also a fully-featured React app that consumes modules exposed by remotes.
Can You Combine Them? (Yes β and many do in 2025)
The most powerful production setup today is:
- single-spa shell (for routing, layout, true isolation)
- Module Federation (only inside each individual MFE for internal code sharing)
Or even:
single-spa shell loads Module Federation remotes via () => System.import('cart/remoteEntry.js')
This gives you the best of both worlds.
Real 2025 Recommendations (Who Should Use What)
| Your Situation | Recommended Approach | Why |
|---|---|---|
| All teams use React + Webpack/Rspack | Pure Module Federation host | Fastest developer experience, great sharing |
| You need React + Vue + Angular in the same app | single-spa shell (with import maps) | Only real way to do true polyglot |
| You are a bank/enterprise with strict governance | single-spa shell | Zero version conflicts, full isolation |
| You want the lightest possible shell (< 20 kB) | single-spa + pure ESM + import maps | Proven at Zalando, DAZN, IKEA |
| You are starting a new greenfield React project | Module Federation host | 80% of new React MFEs in 2025 use this |
| You already have Module Federation but hitting version hell | Migrate shell to single-spa, keep MF for internals | Many companies did this in 2024β2025 |
Final Verdict (2025)
| Question | Answer |
|---|---|
| Are they the same? | No β completely different mechanisms |
| Does one replace the other? | No β they solve different primary problems |
| Which one is more popular today? | Module Federation (among React teams) |
| Which is technically superior for true independence? | single-spa |
| Can you use both at the same time? | Yes β and itβs a very common production pattern |
Bottom line:
- Use Module Federation host β if you are 90β100% React and want speed.
- Use single-spa shell β if you want bulletproof isolation, polyglot support, or the smallest possible orchestrator.
Most companies that outgrow the "React-only" phase eventually add a thin single-spa layer on top of their Module Federation setup.