
Micro Frontends: Can You Split the Frontend Too?
Backends split into microservices, but frontends stay as one giant monolith. Micro frontends solve that — here's what they fix, how composition works, and when it's actually worth it.

Backends split into microservices, but frontends stay as one giant monolith. Micro frontends solve that — here's what they fix, how composition works, and when it's actually worth it.
Tired of naming classes? Writing CSS directly inside HTML sounds ugly, but it became the world standard. Why?

Why is the CPU fast but the computer slow? I explore the revolutionary idea of the 80-year-old Von Neumann architecture and the fatal bottleneck it left behind.

For visually impaired, for keyboard users, and for your future self. Small `alt` tag makes a big difference.

Class is there, but style is missing? Debugging Tailwind CSS like a detective.

When teams were 10 engineers, one React app was fine. Then teams grew to 50, features hit 30+, and PRs started stacking up daily. The checkout team touching checkout code would somehow affect the profile team's build. One bad commit held up everyone's deployment. Bundle size bloated and build times ballooned from 5 to 15 minutes.
Backends already solved this with microservices. Why was the frontend still a monolith?
That's the question micro frontends answer. One sentence: applying microservices philosophy to the frontend.
In a monolithic frontend, the payments team and the search team share a codebase. Code conflicts, review bottlenecks, deployment coordination — all rooted here.
Micro frontends let each team own a fully independent application.
Team structure example:
├── Team Shell (app shell / routing)
├── Team Checkout (payment flow)
├── Team Search (search + filtering)
├── Team Profile (user accounts)
└── Team Marketing (landing pages)
Each team: their own repo, their own pipeline, their own tech choices.
If the checkout team finishes a feature, they should ship it without coordinating with anyone else. Micro frontends make that real.
Deployment comparison:
Monolith:
checkout feature done → build entire app → QA → deploy (2 hours)
Micro frontends:
checkout feature done → build checkout MF → deploy (10 min)
The legacy team built in Angular. The new team wants React. Impossible in a monolith. Possible with micro frontends — with tradeoffs, but possible.
When do you stitch the pieces together? That determines the approach.
All micro apps are bundled as npm packages and merged into one app at build time.
// shell/package.json
{
"dependencies": {
"@mycompany/checkout": "^2.1.0",
"@mycompany/search": "^1.4.0",
"@mycompany/profile": "^3.0.0"
}
}
// shell/src/App.tsx
import { CheckoutApp } from "@mycompany/checkout";
import { SearchApp } from "@mycompany/search";
export default function App() {
return (
<Router>
<Route path="/checkout" component={CheckoutApp} />
<Route path="/search" component={SearchApp} />
</Router>
);
}
Pros: Simple. Type-safe. Easy to optimize bundle.
Cons: No independent deployment. Changing one package requires rebuilding the shell. Version management gets messy.
Use when: Team is small and simplicity beats deployment independence.
This is real micro frontends. Apps load dynamically at runtime. The mechanism for this is Module Federation.
User visits /checkout →
Shell loads checkout.mycompany.com/remoteEntry.js →
Dynamically imports CheckoutApp component →
Renders
Each micro app is deployed independently. Shell fetches at runtime. Checkout team ships a new version? No shell redeploy needed — change is live immediately.
Server fetches HTML fragments from each micro app and assembles them before sending to the client.
Request →
Composition server →
Fetch HTML fragment from header team's server
Fetch HTML fragment from search team's server
Fetch HTML fragment from footer team's server
→ Assemble → Response
Can use SSI, Edge Side Includes, or Next.js + Edge Functions.
Pros: Perfect SEO. Fast initial load. Cons: Server infrastructure complexity. Each team needs SSR support.
Module Federation shipped in webpack 5 and is also supported in rspack. It's the de facto standard for runtime micro frontend composition.
// checkout/webpack.config.js (Remote)
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "checkout",
filename: "remoteEntry.js",
exposes: {
"./CheckoutApp": "./src/CheckoutApp",
"./useCart": "./src/hooks/useCart",
},
shared: {
react: { singleton: true, requiredVersion: "^19.0.0" },
"react-dom": { singleton: true, requiredVersion: "^19.0.0" },
},
}),
],
};
// shell/webpack.config.js (Host)
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "shell",
remotes: {
checkout: "checkout@https://checkout.mycompany.com/remoteEntry.js",
search: "search@https://search.mycompany.com/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^19.0.0" },
"react-dom": { singleton: true, requiredVersion: "^19.0.0" },
},
}),
],
};
// shell/src/pages/CheckoutPage.tsx
import React, { Suspense, lazy } from "react";
// Dynamic import — loaded from checkout server at runtime
const CheckoutApp = lazy(() => import("checkout/CheckoutApp"));
export default function CheckoutPage() {
return (
<Suspense fallback={<div>Loading checkout module...</div>}>
<CheckoutApp />
</Suspense>
);
}
rspack is a Rust-based webpack-compatible bundler. Same Module Federation support, 5-10x faster builds.
// rspack.config.js
const { ModuleFederationPlugin } = require("@module-federation/enhanced/rspack");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "checkout",
filename: "remoteEntry.js",
exposes: {
"./CheckoutApp": "./src/CheckoutApp",
},
shared: ["react", "react-dom"],
}),
],
};
One of the trickier parts of micro frontends.
React and React DOM must be singletons. Two versions loaded simultaneously cause mysterious "Invalid hook call" errors.
shared: {
react: {
singleton: true, // Block multiple versions
requiredVersion: "^19.0.0",
eager: false, // Allow async loading
},
"react-dom": {
singleton: true,
requiredVersion: "^19.0.0",
},
"@mycompany/design-system": {
singleton: true,
requiredVersion: "^5.0.0",
},
}
[ModuleFederation] Shared module react@18.0.0 is already provided
by host. Version 19.0.0 from remote checkout will not be used.
This warning means teams need to align on versions. Don't ignore it.
Each micro app may have internal routing that shouldn't conflict with the shell.
// checkout/src/CheckoutApp.tsx
import { MemoryRouter, Route, Routes } from "react-router-dom";
export default function CheckoutApp({ basePath }: { basePath: string }) {
return (
<MemoryRouter initialEntries={[basePath]}>
<Routes>
<Route path="/checkout/cart" element={<CartPage />} />
<Route path="/checkout/payment" element={<PaymentPage />} />
<Route path="/checkout/confirm" element={<ConfirmPage />} />
</Routes>
</MemoryRouter>
);
}
Don't import between micro apps directly. Use custom events.
// shared/src/eventBus.ts
type EventMap = {
"cart:updated": { itemCount: number; total: number };
"user:logged-in": { userId: string; name: string };
"navigation:push": { path: string };
};
class EventBus {
private handlers: Map<string, Set<Function>> = new Map();
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) {
const fns = this.handlers.get(event);
fns?.forEach((fn) => fn(payload));
}
on<K extends keyof EventMap>(
event: K,
handler: (payload: EventMap[K]) => void
) {
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
this.handlers.get(event)!.add(handler);
return () => this.handlers.get(event)!.delete(handler);
}
}
export const eventBus = new EventBus();
Micro frontends are not always the answer.
When micro frontends make sense:
✅ 5+ teams, each owning a frontend domain
✅ Org is structured around domain teams
✅ Deployment independence is a business requirement
✅ Tech stack heterogeneity is unavoidable
When it's overkill:
❌ Team is 2-3 people
❌ One team owns the entire frontend
❌ Unified UX consistency is the top priority
❌ No DevOps capacity to manage the infrastructure
| Dimension | Monolithic Frontend | Micro Frontends |
|---|---|---|
| Team independence | Low | High |
| Independent deployment | None | Per-team |
| Tech stack | Unified | Team choice |
| Initial setup cost | Low | High |
| Operational complexity | Low | High |
| Bundle optimization | Easy | Hard |
| DX | Simple | Complex |
| Suitable team size | ~10 | 20+ |
Micro frontends are an organizational scaling problem solved with architecture. If your team is 10 engineers and you adopt micro frontends, you just added complexity without benefit.
But if you have multiple teams that need to move independently, and the frontend monolith is genuinely the bottleneck — micro frontends are a powerful fit.
Start with Module Federation. Pick one domain (like checkout) and extract it first. Build the pattern before splitting everything. Incremental beats big bang every time.