1. Introduction to Micro-Frontends
Micro-frontends extend the concepts of microservices to the frontend world. Instead of a monolithic frontend application, you break it into smaller, independent applications that can be developed, deployed, and scaled separately.
๐ฏ Core Problem It Solves
As frontend applications grow, they become:
- โ Hard to maintain (millions of lines of code)
- โ Slow to build (30+ minutes CI/CD pipelines)
- โ Riskier to deploy (one bug breaks everything)
- โ Difficult to scale teams (100+ developers on same codebase)
- โ Technology locked (can't upgrade frameworks incrementally)
Independent Deploy
Deploy each micro-frontend separately
Faster Builds
Build only what changed (seconds vs minutes)
Isolation
Failures don't cascade across teams
๐ก Pro Tip
โ ๏ธ Not for Everyone
Micro-frontends add complexity. For small teams (<10 developers) or simple applications (<50 screens), a well-organized monolith is often better. Use this decision framework at the end of this guide.
2. 5 Core Integration Patterns
// Each micro-frontend published as NPM package
// Container app installs all dependencies
// package.json (Container App)
{
"dependencies": {
"@team/checkout": "^2.1.0",
"@team/products": "^1.4.0",
"@team/reviews": "^3.0.0"
}
}
// Usage in container
import CheckoutModule from '@team/checkout';
import ProductsModule from '@team/products';โ Simple, familiar | โ Requires rebuild for any change | โ No independent deployment
<iframe src="https://checkout.myapp.com"
id="checkout-iframe"
sandbox="allow-same-origin allow-scripts">
</iframe>
// Communication via postMessage
const iframe = document.getElementById('checkout-iframe');
iframe.contentWindow.postMessage({ type: 'ADD_TO_CART', data: item }, '*');โ Strong isolation | โ Poor performance | โ Hard to communicate | โ SEO issues
// Micro-frontend exports as Custom Element
class ProductList extends HTMLElement {
connectedCallback() {
this.render();
}
}
customElements.define('product-list', ProductList);
// Container app uses directly
<product-list category="electronics"></product-list>โ Framework-agnostic | โ Native browser support | โ Bundle duplication | โ Limited SSR
// Webpack config (micro-frontend)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: {
'./Module': './src/CheckoutModule'
},
shared: ['react', 'react-dom']
})
]
};
// Container app consumes dynamically
const CheckoutModule = await import('checkout/Module');โ Runtime integration | โ Share dependencies | โ Independent deploy | โ Best balance
<html>
<body>
<esi:include src="http://products.api/header" />
<esi:include src="http://checkout.api/cart" />
<esi:include src="http://reviews.api/footer" />
</body>
</html>โ Edge-level composition | โ Fast TTFB | โ Complex cache invalidation | โ Limited support
3. Webpack Module Federation Deep Dive
Module Federation is the most popular pattern for micro-frontends in 2024. Let's build a complete example.
// Step 1: Host/Container App Configuration
// webpack.config.js (Container)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
cart: 'cart@http://localhost:3002/remoteEntry.js',
checkout: 'checkout@http://localhost:3003/remoteEntry.js'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
'react-router-dom': { singleton: true }
}
})
]
};// Step 2: Remote Micro-Frontend Configuration
// webpack.config.js (Remote - Products App)
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
'./store': './src/store/productStore'
},
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true },
zustand: { singleton: true }
}
})// Step 3: Using Remote Module in Container
// App.js (Container)
import React, { lazy, Suspense } from 'react';
// Lazy load remote components
const ProductList = lazy(() => import('products/ProductList'));
const Cart = lazy(() => import('cart/Cart'));
function App() {
return (
<Suspense fallback=<div>Loading...</div>>
<ProductList category="electronics" />
<Cart />
</Suspense>
);
}๐ Info
๐ก Pro Tip: Version Mismatch Handling
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
version: '18.2.0',
eager: false
}
}
if (!window.React18) {
await import('remote/React18Fallback');
}4. Container App Orchestration
The container (shell/host) app orchestrates all micro-frontends. Here's a production-ready orchestrator:
// orchestration/ModuleLoader.js
class ModuleLoader {
constructor() {
this.registry = new Map();
this.loadingPromises = new Map();
}
async loadModule(moduleName, remoteUrl) {
if (this.registry.has(moduleName)) {
return this.registry.get(moduleName);
}
if (this.loadingPromises.has(moduleName)) {
return this.loadingPromises.get(moduleName);
}
const loadPromise = this._loadRemoteModule(moduleName, remoteUrl);
this.loadingPromises.set(moduleName, loadPromise);
try {
const module = await loadPromise;
this.registry.set(moduleName, module);
return module;
} finally {
this.loadingPromises.delete(moduleName);
}
}
async _loadRemoteModule(name, url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.type = 'text/javascript';
script.async = true;
script.onload = () => {
const module = window[name];
if (module) resolve(module);
else reject(new Error(`Module ${name} not found`));
};
script.onerror = () => reject(new Error(`Failed to load ${url}`));
document.head.appendChild(script);
});
}
}5. Inter-Application Communication
๐ก Event Bus Pattern
class EventBus {
constructor() {
this.listeners = new Map();
}
emit(event, data) {
const callbacks = this.listeners.get(event) || [];
callbacks.forEach(cb => cb(data));
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return () => {
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) callbacks.splice(index, 1);
};
}
}
export const globalBus = new EventBus();๐ฆ Shared Custom Elements API
window.MicroFrontendAPI = {
state: { user: null, cart: [], theme: 'light' },
dispatch(action, payload) {
switch(action) {
case 'ADD_TO_CART':
this.state.cart.push(payload);
this.notify('cart', this.state.cart);
break;
}
},
register(name, { mount, unmount }) {
this.registry[name] = { mount, unmount };
}
};โ Good to Know
โ Best Practice: Custom Events for Loose Coupling
window.dispatchEvent(new CustomEvent('cart:updated', {
detail: { items: cartItems, total: 299 }
}));
window.addEventListener('cart:updated', (event) => {
const { items, total } = event.detail;
updateUI(items, total);
});6. Routing & Navigation Strategies
Primary Router (Container) + Secondary Routers (M-FEs)
function ContainerApp() {
return (
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products/*" element={
<MFERoute prefix="/products"><ProductApp /></MFERoute>
} />
</Routes>
</Layout>
</BrowserRouter>
);
}7. Styling Isolation & CSS Strategies
โ Strategy 1: CSS Modules
// Button.module.css
.button { background: var(--primary-color); }
import styles from './Button.module.css';
<button className={styles.button}>Click</button>โ Strategy 2: CSS-in-JS
import styled from '@emotion/styled';
const Button = styled.button`
background: var(--primary);
&:hover { background: var(--primary-dark); }
`;โ Strategy 3: Shadow DOM
class IsolatedComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `<style>.button { background: blue; }</style><button>Click</button>`;
}
}8. Shared State Management
๐ Option A: Shared Store (Zustand)
import { create } from 'zustand';
export const useStore = create((set) => ({
user: null,
cart: [],
setUser: (user) => set({ user }),
addToCart: (item) => set((state) => ({ cart: [...state.cart, item] }))
}));๐พ Option B: Context Sharing
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return <AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>;
};9. Independent Deployment Strategies
// CI/CD Pipeline for Single Micro-Frontend
name: Deploy Products MFE
on:
push:
paths: ['apps/products/**']
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
- run: aws s3 sync dist/ s3://mfe-cdn/products/
- run: curl -X POST https://registry.myapp.com/modules/products10. Team Structures & Ownership Models
Vertical Teams (Recommended)
- Team owns feature end-to-end
- Full autonomy, high ownership
Platform Team
- Owns container/base infrastructure
- Shared components library
Enablement Team
- Governance, standards
- Developer experience
11. Performance Optimization
โก Bundle Sharing Optimization
new ModuleFederationPlugin({
shared: { react: { singleton: true, requiredVersion: '^18.0.0', eager: false } }
});โ Good to Know
๐ Performance Budget Targets
| Metric | Target | Hard Limit |
|---|---|---|
| Initial Bundle | <200KB | 300KB |
| Per M-FE Bundle | <100KB | 150KB |
| LCP | <2.5s | 4s |
13. Real-World Case Study: E-Commerce Platform
Company: MegaShop (1M+ daily users)
Before Micro-Frontends:
- โ 4 hour build times
- โ 300+ developers on same codebase
After Micro-Frontends:
- โ 3-5 minute builds per M-FE
- โ 8 independent teams
16. Decision Framework: When to Adopt
Decision Tree:
START: Team size > 25 developers?
โโ NO โ Don't adopt
โโ YES โ Build time > 10 minutes?
โโ NO โ Consider carefully
โโ YES โ โ
Strong candidateReady to Start Your Micro-Frontends Journey?
Get our complete starter kit with Webpack Module Federation, CI/CD pipeline, and production-ready patterns
