Your frontend monolith is getting too big. Deployments take forever. Teams step on each other. Every change risks breaking something unrelated.
Micro frontends promise to fix this by letting teams own and deploy pieces independently. But like microservices, theyre not free. Lets figure out when they make sense.
What Are Micro Frontends?
Instead of one big app, you have multiple smaller apps that compose into one:
Each micro frontend:
- Has its own repo and CI/CD
- Can use different frameworks (but probably shouldnt)
- Deploys independently
- Owned by one team
Integration Approaches
1. Build-Time Integration (Module Federation)
Webpack 5 Module Federation is the modern way:
// products-mfe/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/ProductList',
'./ProductDetail': './src/ProductDetail',
},
shared: ['react', 'react-dom'],
}),
],
};
// shell-app/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
products: 'products@https://products.example.com/remoteEntry.js',
cart: 'cart@https://cart.example.com/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
};
// Shell app loads remote components
const ProductList = React.lazy(() => import('products/ProductList'));
function App() {
return (
<Suspense fallback={<Loading />}>
<ProductList />
</Suspense>
);
}
2. Runtime Integration (iframes)
Old school but still works:
Pros: Complete isolation, any framework Cons: Performance, communication is awkward, styling issues
3. Web Components
Framework-agnostic custom elements:
// products-mfe/product-card.ts
class ProductCard extends HTMLElement {
connectedCallback() {
const productId = this.getAttribute('product-id');
this.innerHTML = `<div class="product">...</div>`;
}
}
customElements.define('product-card', ProductCard);
// Shell app
<product-card product-id="123"></product-card>
Communication Between MFEs
// Shared event bus
const eventBus = {
events: new Map<string, Function[]>(),
subscribe(event: string, callback: Function) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(callback);
},
publish(event: string, data: any) {
const callbacks = this.events.get(event) || [];
callbacks.forEach(cb => cb(data));
}
};
// Cart MFE
eventBus.publish('cart:updated', { itemCount: 3 });
// Header MFE
eventBus.subscribe('cart:updated', ({ itemCount }) => {
updateBadge(itemCount);
});
When Micro Frontends Make Sense
Good fit:
- Multiple teams working on same product
- Teams need to deploy independently
- Different parts evolve at different speeds
- You have the infrastructure maturity
Bad fit:
- Small team (under 10 devs)
- Simple application
- Tight deadlines (adds upfront complexity)
- No DevOps support
The Gotchas
1. Shared Dependencies
Multiple copies of React = bloated bundle:
// Without sharing: React loaded 4 times = 400KB
// With Module Federation sharing:
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
}
2. Styling Conflicts
CSS leaks between MFEs:
/* Products MFE */
.button { background: blue; }
/* Cart MFE */
.button { background: green; }
/* Chaos! */
Solutions:
- CSS Modules
- CSS-in-JS with unique prefixes
- Shadow DOM
3. Consistent UX
Each team builds slightly different buttons, modals, etc.
Solution: Shared design system as a package:
// @company/design-system
import { Button, Modal, Card } from '@company/design-system';
Simpler Alternatives
Before going full micro frontend:
- Monorepo with good boundaries - Nx, Turborepo
- Feature flags - Deploy together, release separately
- Plugin architecture - Load features dynamically
Quick Checklist
Before adopting micro frontends:
- [ ] 3+ teams working on frontend
- [ ] Clear domain boundaries
- [ ] Teams have DevOps capabilities
- [ ] Design system exists (or planned)
- [ ] Accepted the complexity tradeoff
Further Reading
Micro frontends solve real problems for large organizations. But they add significant complexity. Make sure you actually have the problems they solve before adopting them.
