Advanced Micro Frontend Patterns: Performance, Debugging, and Production Lessons
Advanced micro frontend patterns: event-sourced shared state, bundle budgets, cross-app error tracing, CSP and signed messaging, and strangler-fig migration.
Micro Frontend Series Navigation
- Part 1: Architecture fundamentals and implementation types
- Part 2: Module Federation, communication patterns, and integration strategies
- Part 3 (You are here): Advanced patterns, performance optimization, and production debugging
Prerequisites: Part 1 fundamentals and Part 2 implementation patterns.
Micro frontends in production introduce hard problems that architecture diagrams rarely show: shared state diverging across independently deployed apps, bundle duplication inflating load times, and errors that cross team and deployment boundaries. Three patterns carry most of that weight. Event sourcing keeps shared state consistent across independently deployed apps, performance budgeting contains bundle duplication, and cross-app error tracing makes distributed frontend failures debuggable.
Advanced State Management Patterns
The hardest part of a micro frontend architecture is shared state across independently deployed applications. The two patterns below keep that state consistent without coupling deploys.
1. Distributed State Management with Event Sourcing
// @company/shared-state - Advanced state management for micro frontends
interface StateEvent {
id: string;
type: string;
payload: any;
timestamp: number;
version: number;
microfrontend: string;
}
interface StateSnapshot {
version: number;
data: any;
timestamp: number;
}
class DistributedStateManager {
private eventStore: StateEvent[] = [];
private snapshots: Map<string, StateSnapshot> = new Map();
private subscribers: Map<string, Set<(state: any) => void>> = new Map();
private maxEventHistory = 1000;
private snapshotInterval = 100; // Create snapshot every 100 events
constructor(private persistenceAdapter?: PersistenceAdapter) {
// Load initial state from persistence layer
this.loadInitialState();
}
// Append-only event store
dispatch(event: Omit<StateEvent, 'id' | 'timestamp' | 'version'>) {
const stateEvent: StateEvent = {
...event,
id: generateId(),
timestamp: Date.now(),
version: this.eventStore.length + 1,
};
this.eventStore.push(stateEvent);
// Create periodic snapshots for performance
if (stateEvent.version % this.snapshotInterval === 0) {
this.createSnapshot(event.type.split(':')[0]); // namespace from event type
}
// Persist to external storage
this.persistenceAdapter?.persistEvent(stateEvent);
// Notify subscribers
this.notifySubscribers(event.type, this.getState(event.type.split(':')[0]));
// Maintain event history size
if (this.eventStore.length > this.maxEventHistory) {
this.eventStore = this.eventStore.slice(-this.maxEventHistory);
}
}
getState(namespace: string): any {
// Try to use latest snapshot first
const snapshot = this.snapshots.get(namespace);
const eventsAfterSnapshot = snapshot
? this.eventStore.filter(e => e.version > snapshot.version && e.type.startsWith(namespace))
: this.eventStore.filter(e => e.type.startsWith(namespace));
let state = snapshot?.data || {};
// Apply events after snapshot
eventsAfterSnapshot.forEach(event => {
state = this.applyEvent(state, event);
});
return state;
}
subscribe(eventType: string, callback: (state: any) => void): () => void {
if (!this.subscribers.has(eventType)) {
this.subscribers.set(eventType, new Set());
}
this.subscribers.get(eventType)!.add(callback);
// Immediately call with current state
const namespace = eventType.split(':')[0];
callback(this.getState(namespace));
// Return unsubscribe function
return () => {
this.subscribers.get(eventType)?.delete(callback);
};
}
private applyEvent(state: any, event: StateEvent): any {
switch (event.type) {
case 'user:login':
return { ...state, user: event.payload, isAuthenticated: true };
case 'user:logout':
return { ...state, user: null, isAuthenticated: false };
case 'cart:add-item':
const items = state.items || [];
return {
...state,
items: [...items, event.payload],
total: calculateTotal([...items, event.payload])
};
case 'cart:remove-item':
const filteredItems = (state.items || []).filter(
(item: any) => item.id !== event.payload.id
);
return {
...state,
items: filteredItems,
total: calculateTotal(filteredItems)
};
default:
return state;
}
}
private createSnapshot(namespace: string) {
const state = this.getState(namespace);
const snapshot: StateSnapshot = {
version: this.eventStore.length,
data: state,
timestamp: Date.now(),
};
this.snapshots.set(namespace, snapshot);
this.persistenceAdapter?.persistSnapshot(namespace, snapshot);
}
private notifySubscribers(eventType: string, state: any) {
// Exact subscribers plus namespace wildcards such as "cart:*"
const namespace = eventType.split(':')[0];
const callbacks = new Set([
...(this.subscribers.get(eventType) ?? []),
...(this.subscribers.get(`${namespace}:*`) ?? []),
]);
callbacks.forEach(callback => {
try {
callback(state);
} catch (error) {
console.error(`Error in state subscriber for ${eventType}:`, error);
}
});
}
private async loadInitialState() {
if (this.persistenceAdapter) {
try {
const { events, snapshots } = await this.persistenceAdapter.loadInitialState();
this.eventStore = events;
snapshots.forEach((snapshot, namespace) => {
this.snapshots.set(namespace, snapshot);
});
} catch (error) {
console.error('Failed to load initial state:', error);
}
}
}
// Debug utilities
getEventHistory(namespace?: string): StateEvent[] {
if (namespace) {
return this.eventStore.filter(e => e.type.startsWith(namespace));
}
return [...this.eventStore];
}
replayEventsFrom(version: number): void {
const eventsToReplay = this.eventStore.filter(e => e.version >= version);
console.log(`Replaying ${eventsToReplay.length} events from version ${version}`);
eventsToReplay.forEach(event => {
console.log(`Replaying: ${event.type}`, event.payload);
});
}
}
// Persistence adapter interface
interface PersistenceAdapter {
persistEvent(event: StateEvent): Promise<void>;
persistSnapshot(namespace: string, snapshot: StateSnapshot): Promise<void>;
loadInitialState(): Promise<{
events: StateEvent[];
snapshots: Map<string, StateSnapshot>;
}>;
}
// React hooks for easier usage
export const useDistributedState = <T>(namespace: string): [T, (event: any) => void] => {
const [state, setState] = useState<T>({} as T);
const stateManager = useContext(StateManagerContext);
useEffect(() => {
const unsubscribe = stateManager.subscribe(`${namespace}:*`, setState);
return unsubscribe;
}, [namespace, stateManager]);
const dispatch = useCallback((event: any) => {
stateManager.dispatch({
type: `${namespace}:${event.type}`,
payload: event.payload,
microfrontend: event.source || 'unknown',
});
}, [namespace, stateManager]);
return [state, dispatch];
};
2. Optimistic Updates with Conflict Resolution
// Advanced optimistic update pattern for micro frontends
interface OptimisticUpdate {
id: string;
type: string;
payload: any;
timestamp: number;
microfrontend: string;
status: 'pending' | 'confirmed' | 'failed';
}
class OptimisticStateManager {
private pendingUpdates: Map<string, OptimisticUpdate> = new Map();
private baseState: any = {};
private stateManager: DistributedStateManager;
constructor(stateManager: DistributedStateManager) {
this.stateManager = stateManager;
}
optimisticDispatch(event: any): string {
const updateId = generateId();
const optimisticUpdate: OptimisticUpdate = {
id: updateId,
type: event.type,
payload: event.payload,
timestamp: Date.now(),
microfrontend: event.source,
status: 'pending',
};
this.pendingUpdates.set(updateId, optimisticUpdate);
// Apply optimistic update immediately
this.applyOptimisticUpdate(optimisticUpdate);
// Send to server
this.sendToServer(event)
.then(() => {
// Confirm the update
const update = this.pendingUpdates.get(updateId);
if (update) {
update.status = 'confirmed';
// Apply confirmed state
this.stateManager.dispatch(event);
}
})
.catch((error) => {
// Revert optimistic update
const update = this.pendingUpdates.get(updateId);
if (update) {
update.status = 'failed';
this.revertOptimisticUpdate(updateId);
// Show error to user
this.showConflictResolution(error, event);
}
})
.finally(() => {
this.pendingUpdates.delete(updateId);
});
return updateId;
}
private applyOptimisticUpdate(update: OptimisticUpdate) {
// Apply the update optimistically to the UI
const event = {
type: update.type,
payload: { ...update.payload, _optimistic: true },
microfrontend: update.microfrontend,
};
this.stateManager.dispatch(event);
}
private revertOptimisticUpdate(updateId: string) {
const update = this.pendingUpdates.get(updateId);
if (!update) return;
// Dispatch revert event
this.stateManager.dispatch({
type: `${update.type}:revert`,
payload: { originalPayload: update.payload },
microfrontend: update.microfrontend,
});
}
private async sendToServer(event: any): Promise<any> {
const response = await fetch('/api/state/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
if (!response.ok) {
throw new Error(`Server rejected update: ${response.statusText}`);
}
return response.json();
}
private showConflictResolution(error: Error, originalEvent: any) {
// Show UI for conflict resolution
this.stateManager.dispatch({
type: 'ui:show-conflict-resolution',
payload: {
error: error.message,
originalEvent,
timestamp: Date.now(),
},
microfrontend: 'system',
});
}
}
Performance Optimization Strategies
1. Advanced Bundle Analysis and Optimization
// webpack-bundle-analyzer-reporter.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
class MicroFrontendBundleReporter {
constructor(options = {}) {
this.options = {
reportDir: './bundle-reports',
threshold: 50000, // 50KB
...options,
};
}
apply(compiler) {
compiler.hooks.emit.tapAsync('MicroFrontendBundleReporter', (compilation, callback) => {
const stats = compilation.getStats().toJson();
const analysis = this.analyzeBundle(stats);
// Generate report
this.generateReport(analysis);
// Warn about large bundles
this.checkBundleSize(analysis);
callback();
});
}
analyzeBundle(stats) {
const analysis = {
totalSize: 0,
sharedDependencies: {},
duplicatedDependencies: [],
largeDependencies: [],
unusedExports: [],
};
stats.chunks.forEach(chunk => {
chunk.modules.forEach(module => {
const size = module.size || 0;
analysis.totalSize += size;
// Identify shared dependencies
if (module.name && module.name.includes('node_modules')) {
const dep = this.extractDependencyName(module.name);
if (!analysis.sharedDependencies[dep]) {
analysis.sharedDependencies[dep] = { size: 0, count: 0 };
}
analysis.sharedDependencies[dep].size += size;
analysis.sharedDependencies[dep].count++;
}
// Flag large dependencies
if (size > this.options.threshold) {
analysis.largeDependencies.push({
name: module.name,
size,
reasons: module.reasons || [],
});
}
});
});
// Find duplicated dependencies
Object.entries(analysis.sharedDependencies).forEach(([dep, info]) => {
if (info.count > 1) {
analysis.duplicatedDependencies.push({ name: dep, ...info });
}
});
return analysis;
}
generateReport(analysis) {
const report = {
timestamp: new Date().toISOString(),
microfrontend: process.env.MICRO_FRONTEND_NAME || 'unknown',
...analysis,
};
const fs = require('fs');
const path = require('path');
if (!fs.existsSync(this.options.reportDir)) {
fs.mkdirSync(this.options.reportDir, { recursive: true });
}
const reportPath = path.join(
this.options.reportDir,
`bundle-report-${Date.now()}.json`
);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`Bundle report generated: ${reportPath}`);
}
checkBundleSize(analysis) {
const maxSize = 500000; // 500KB warning threshold
if (analysis.totalSize > maxSize) {
console.warn(`[WARN] Bundle size (${analysis.totalSize} bytes) exceeds recommended threshold (${maxSize} bytes)`);
}
if (analysis.duplicatedDependencies.length > 0) {
console.warn('[WARN] Duplicated dependencies found:');
analysis.duplicatedDependencies.forEach(dep => {
console.warn(` - ${dep.name}: ${dep.size} bytes (${dep.count} copies)`);
});
}
}
extractDependencyName(moduleName) {
const match = moduleName.match(/node_modules[\/\\](@[^\/\\]+[\/\\][^\/\\]+|[^\/\\]+)/);
return match ? match[1] : moduleName;
}
}
module.exports = MicroFrontendBundleReporter;
2. Performance Monitoring and Optimization
// @company/performance-monitor
interface PerformanceMetrics {
microfrontend: string;
loadTime: number;
renderTime: number;
bundleSize: number;
memoryUsage: number;
errorCount: number;
}
class MicroFrontendPerformanceMonitor {
private metrics: Map<string, PerformanceMetrics> = new Map();
private observers: PerformanceObserver[] = [];
constructor(private reportingEndpoint?: string) {
this.setupPerformanceObservers();
}
startMeasurement(microfrontendName: string) {
const startTime = performance.now();
return {
recordLoadComplete: () => {
const loadTime = performance.now() - startTime;
this.updateMetrics(microfrontendName, { loadTime });
},
recordRenderComplete: () => {
const renderTime = performance.now() - startTime;
this.updateMetrics(microfrontendName, { renderTime });
},
recordError: () => {
const current = this.metrics.get(microfrontendName);
this.updateMetrics(microfrontendName, {
errorCount: (current?.errorCount || 0) + 1
});
}
};
}
private setupPerformanceObservers() {
// Monitor resource loading
const resourceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name.includes('remoteEntry.js')) {
const microfrontendName = this.extractMicrofrontendName(entry.name);
this.updateMetrics(microfrontendName, {
bundleSize: entry.transferSize || 0,
});
}
});
});
resourceObserver.observe({ entryTypes: ['resource'] });
this.observers.push(resourceObserver);
// Monitor long tasks
const longTaskObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 16) { // Tasks longer than 16ms (1 frame at 60fps)
console.warn(`Long task detected: ${entry.duration}ms`);
this.reportLongTask(entry);
}
});
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
this.observers.push(longTaskObserver);
// Monitor memory usage
this.startMemoryMonitoring();
}
private startMemoryMonitoring() {
setInterval(() => {
if ('memory' in performance) {
const memInfo = (performance as any).memory;
this.metrics.forEach((_, microfrontendName) => {
this.updateMetrics(microfrontendName, {
memoryUsage: memInfo.usedJSHeapSize,
});
});
}
}, 10000); // Every 10 seconds
}
private updateMetrics(microfrontendName: string, updates: Partial<PerformanceMetrics>) {
const current = this.metrics.get(microfrontendName) || {
microfrontend: microfrontendName,
loadTime: 0,
renderTime: 0,
bundleSize: 0,
memoryUsage: 0,
errorCount: 0,
};
const updated = { ...current, ...updates };
this.metrics.set(microfrontendName, updated);
// Report to monitoring service
this.reportMetrics(updated);
}
private reportMetrics(metrics: PerformanceMetrics) {
if (this.reportingEndpoint) {
fetch(this.reportingEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metrics),
}).catch(error => {
console.error('Failed to report metrics:', error);
});
}
// Also log to console in development
if (process.env.NODE_ENV === 'development') {
console.log(`[Performance] ${metrics.microfrontend}:`, metrics);
}
}
private reportLongTask(entry: PerformanceEntry) {
if (this.reportingEndpoint) {
fetch(`${this.reportingEndpoint}/long-tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
duration: entry.duration,
startTime: entry.startTime,
timestamp: Date.now(),
}),
}).catch(error => {
console.error('Failed to report long task:', error);
});
}
}
getMetrics(microfrontendName?: string): PerformanceMetrics | Map<string, PerformanceMetrics> {
if (microfrontendName) {
return this.metrics.get(microfrontendName)!;
}
return new Map(this.metrics);
}
cleanup() {
this.observers.forEach(observer => observer.disconnect());
}
private extractMicrofrontendName(url: string): string {
const match = url.match(/\/\/([^.]+)\./);
return match ? match[1] : 'unknown';
}
}
// React hook for performance monitoring
export const usePerformanceMonitor = (microfrontendName: string) => {
const monitor = useContext(PerformanceMonitorContext);
useEffect(() => {
const measurement = monitor.startMeasurement(microfrontendName);
// Record when component mounts (render complete)
measurement.recordRenderComplete();
return () => {
// Cleanup if needed
};
}, [microfrontendName, monitor]);
const recordError = useCallback(() => {
const measurement = monitor.startMeasurement(microfrontendName);
measurement.recordError();
}, [microfrontendName, monitor]);
return { recordError };
};
Debugging Patterns for Distributed Frontends
Memory Leaks from Event Bus Subscriptions
A leak that only appears after hours of continuous use is the classic failure mode of a shared event bus. The application gets gradually slower, and eventually some micro frontends stop responding. The usual cause is a subscription that outlives the component that registered it, keeping the whole component scope alive:
// The problematic code that caused memory leaks
const ProductList: React.FC = () => {
const eventBus = useContext(EventBusContext);
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
// This was creating a closure that held references to the entire component
const handler = (event: any) => {
// The handler closure captured the entire component scope
setProducts(prevProducts => {
// Complex state updates that held onto old state
return updateProductsWithComplexLogic(prevProducts, event);
});
};
eventBus.subscribe('cart:updated', handler);
// The unsubscribe was never called due to early returns in some code paths
return () => eventBus.unsubscribe('cart:updated', handler);
}, []); // Missing dependencies caused stale closures
return <div className="product-list">{/* ... */}</div>;
};
The fix is a stable handler reference plus a cleanup path that always runs:
// Fixed version with proper memory management
const ProductList: React.FC = () => {
const eventBus = useContext(EventBusContext);
const [products, setProducts] = useState<Product[]>([]);
// Use useCallback to prevent unnecessary recreations
const handleCartUpdate = useCallback((event: CartUpdateEvent) => {
setProducts(prevProducts => {
// Use immutable updates to prevent reference cycles
return prevProducts.map(product =>
product.id === event.productId
? { ...product, inCart: event.inCart }
: product
);
});
}, []); // No dependencies needed since we use functional updates
useEffect(() => {
const unsubscribe = eventBus.subscribe('cart:updated', handleCartUpdate);
// Always ensure cleanup happens
return () => {
unsubscribe();
};
}, [eventBus, handleCartUpdate]);
// Add memory usage monitoring in development
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
const interval = setInterval(() => {
if ('memory' in performance) {
const memInfo = (performance as any).memory;
console.log(`ProductList memory usage: ${memInfo.usedJSHeapSize / 1024 / 1024} MB`);
}
}, 5000);
return () => clearInterval(interval);
}
}, []);
return (
<div className="product-list">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
// Memory leak detection utility
class MemoryLeakDetector {
private snapshots: any[] = [];
private interval: NodeJS.Timeout;
constructor(private intervalMs: number = 30000) {
this.interval = setInterval(() => {
this.takeSnapshot();
}, intervalMs);
}
private takeSnapshot() {
if ('memory' in performance) {
const memInfo = (performance as any).memory;
const snapshot = {
timestamp: Date.now(),
usedJSHeapSize: memInfo.usedJSHeapSize,
totalJSHeapSize: memInfo.totalJSHeapSize,
jsHeapSizeLimit: memInfo.jsHeapSizeLimit,
};
this.snapshots.push(snapshot);
// Keep only last 20 snapshots
if (this.snapshots.length > 20) {
this.snapshots.shift();
}
this.analyzeMemoryTrend();
}
}
private analyzeMemoryTrend() {
if (this.snapshots.length < 5) return;
const recent = this.snapshots.slice(-5);
// Calculate memory growth rate and pattern
let consecutiveIncreases = 0;
let totalGrowth = 0;
for (let i = 1; i < recent.length; i++) {
const growth = recent[i].usedJSHeapSize - recent[i - 1].usedJSHeapSize;
if (growth > 0) {
consecutiveIncreases++;
totalGrowth += growth;
} else {
consecutiveIncreases = 0; // Reset counter on decrease
}
}
// Detect concerning patterns
const growthMB = totalGrowth / 1024 / 1024;
const hasConsistentGrowth = consecutiveIncreases >= 3;
const hasSignificantGrowth = growthMB > 5; // 5 MB across the window
if (hasConsistentGrowth && hasSignificantGrowth) {
console.warn(`Potential memory leak detected. Growth: ${growthMB.toFixed(2)} MB over ${recent.length - 1} intervals`);
this.reportMemoryLeak(recent);
}
}
private reportMemoryLeak(snapshots: any[]) {
// Report to monitoring service
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track('Memory Leak Detected', {
snapshots,
userAgent: navigator.userAgent,
timestamp: Date.now(),
});
}
}
cleanup() {
if (this.interval) {
clearInterval(this.interval);
}
}
}
Intermittent Cross-Origin Message Loss
Micro frontends deployed on different subdomains lose messages intermittently. postMessage delivers when the target frame is already loaded and silently reaches nothing when it is not. No error is thrown, so the failure looks random. A queue with bounded retries removes the timing dependency, and strict origin checks keep the channel from becoming an entry point:
// The solution: Robust cross-origin communication
class CrossOriginMessenger {
private trustedOrigins: Set<string> = new Set();
private messageQueue: Array<{ data: any; targetOrigin: string; retry: number }> = [];
private maxRetries = 3;
constructor(trustedOrigins: string[]) {
trustedOrigins.forEach(origin => this.trustedOrigins.add(origin));
this.setupMessageListener();
this.startRetryProcessor();
}
private setupMessageListener() {
window.addEventListener('message', (event) => {
// Strict origin checking
if (!this.trustedOrigins.has(event.origin)) {
console.warn(`Rejected message from untrusted origin: ${event.origin}`);
return;
}
try {
const message = JSON.parse(event.data);
this.handleMessage(message, event.origin);
} catch (error) {
console.error('Failed to parse cross-origin message:', error);
}
});
}
sendMessage(data: any, targetOrigin: string, retries: number = 0): boolean {
if (!this.trustedOrigins.has(targetOrigin)) {
console.error(`Attempted to send message to untrusted origin: ${targetOrigin}`);
return false;
}
try {
const serializedData = JSON.stringify({
...data,
timestamp: Date.now(),
sender: window.location.origin,
messageId: generateId(),
});
// Try to find the target window
const targetWindow = this.findTargetWindow(targetOrigin);
if (targetWindow) {
targetWindow.postMessage(serializedData, targetOrigin);
return true;
} else {
// Queue for later retry
this.messageQueue.push({ data, targetOrigin, retry: retries });
return false;
}
} catch (error) {
console.error('Failed to send cross-origin message:', error);
return false;
}
}
private findTargetWindow(targetOrigin: string): Window | null {
// Check all iframes
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
if (iframe.src.startsWith(targetOrigin)) {
return iframe.contentWindow;
}
} catch (error) {
// Access denied - likely cross-origin
continue;
}
}
// Check if it's the parent window
if (window.parent !== window) {
try {
if (document.referrer.startsWith(targetOrigin)) {
return window.parent;
}
} catch (error) {
// Access denied
}
}
return null;
}
private startRetryProcessor() {
setInterval(() => {
const toRetry = this.messageQueue.splice(0); // Take all queued messages
toRetry.forEach(({ data, targetOrigin, retry }) => {
if (retry < this.maxRetries) {
const success = this.sendMessage(data, targetOrigin, retry + 1);
if (!success) {
// Re-queue with incremented retry count
this.messageQueue.push({ data, targetOrigin, retry: retry + 1 });
}
} else {
console.error(`Failed to deliver message after ${this.maxRetries} retries:`, data);
}
});
}, 1000); // Retry every second
}
private handleMessage(message: any, origin: string) {
// Handle different message types
switch (message.type) {
case 'state-update':
this.handleStateUpdate(message.payload);
break;
case 'navigation':
this.handleNavigation(message.payload);
break;
case 'error':
this.handleError(message.payload);
break;
default:
console.warn(`Unknown message type: ${message.type}`);
}
}
private handleStateUpdate(payload: any) {
// Update shared state
if (typeof window !== 'undefined' && (window as any).stateManager) {
(window as any).stateManager.dispatch({
type: payload.type,
payload: payload.data,
source: 'cross-origin',
});
}
}
private handleNavigation(payload: any) {
// Handle navigation requests
if (payload.path && typeof window !== 'undefined') {
window.history.pushState({}, '', payload.path);
}
}
private handleError(payload: any) {
console.error('Cross-origin error received:', payload);
// Report to monitoring service
}
}
Security Considerations for Micro Frontends
1. Content Security Policy (CSP) for Dynamic Loading
// CSP header generator for micro frontend applications
class MicroFrontendCSPGenerator {
constructor(
private allowedOrigins: string[],
private isDevelopment: boolean = false
) {}
generateCSP(): string {
const directives = [];
// Script sources - allow own origin and micro frontend origins
const scriptSrc = [
"'self'",
...this.allowedOrigins,
];
if (this.isDevelopment) {
scriptSrc.push("'unsafe-eval'"); // For development tools
}
directives.push(`script-src ${scriptSrc.join(' ')}`);
// Connect sources for API calls
const connectSrc = [
"'self'",
...this.allowedOrigins,
// Add API endpoints
'https://api.company.com',
];
directives.push(`connect-src ${connectSrc.join(' ')}`);
// Frame sources for iframe-based micro frontends
const frameSrc = [
"'self'",
...this.allowedOrigins,
];
directives.push(`frame-src ${frameSrc.join(' ')}`);
// Image sources
directives.push(`img-src 'self' data: https:`);
// Style sources - use nonce-based approach instead of unsafe-inline
const styleSrc = [
"'self'",
"'nonce-{NONCE_VALUE}'", // Replace with actual nonce in production
...this.allowedOrigins,
];
// Only allow unsafe-inline in development for hot reloading
if (this.isDevelopment) {
styleSrc.push("'unsafe-inline'");
}
directives.push(`style-src ${styleSrc.join(' ')}`);
return directives.join('; ');
}
// Middleware for Express.js
middleware() {
return (req: any, res: any, next: any) => {
res.setHeader('Content-Security-Policy', this.generateCSP());
next();
};
}
}
// Usage
const cspGenerator = new MicroFrontendCSPGenerator([
'https://products.company.com',
'https://cart.company.com',
'https://user.company.com',
], process.env.NODE_ENV === 'development');
app.use(cspGenerator.middleware());
2. Secure Inter-Micro-Frontend Communication
// Secure communication channel
class SecureMicroFrontendCommunication {
private secretKey: string;
private trustedOrigins: Set<string>;
constructor(secretKey: string, trustedOrigins: string[]) {
this.secretKey = secretKey;
this.trustedOrigins = new Set(trustedOrigins);
}
async sendSecureMessage(data: any, targetOrigin: string): Promise<boolean> {
if (!this.trustedOrigins.has(targetOrigin)) {
throw new Error(`Untrusted origin: ${targetOrigin}`);
}
try {
// Create message with timestamp and nonce
const message = {
data,
timestamp: Date.now(),
nonce: this.generateNonce(),
};
// Sign the message
const signature = await this.signMessage(message);
const secureMessage = { ...message, signature };
// Send via postMessage
const targetWindow = this.findTargetWindow(targetOrigin);
if (targetWindow) {
targetWindow.postMessage(JSON.stringify(secureMessage), targetOrigin);
return true;
}
return false;
} catch (error) {
console.error('Failed to send secure message:', error);
return false;
}
}
async verifyAndHandleMessage(event: MessageEvent): Promise<boolean> {
if (!this.trustedOrigins.has(event.origin)) {
console.warn(`Message from untrusted origin: ${event.origin}`);
return false;
}
try {
const message = JSON.parse(event.data);
// Verify timestamp (prevent replay attacks)
const age = Date.now() - message.timestamp;
if (age > 60000) { // 1 minute max age
console.warn('Message too old, potential replay attack');
return false;
}
// Verify signature
const isValid = await this.verifySignature(message);
if (!isValid) {
console.warn('Invalid message signature');
return false;
}
// Process the message
this.handleVerifiedMessage(message.data);
return true;
} catch (error) {
console.error('Failed to verify message:', error);
return false;
}
}
private async signMessage(message: any): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const keyData = encoder.encode(this.secretKey);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, data);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
private async verifySignature(message: any): Promise<boolean> {
const { signature, ...messageWithoutSignature } = message;
const expectedSignature = await this.signMessage(messageWithoutSignature);
return signature === expectedSignature;
}
private generateNonce(): string {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
private findTargetWindow(targetOrigin: string): Window | null {
// Implementation similar to previous example
return null; // Simplified for brevity
}
private handleVerifiedMessage(data: any) {
// Handle the verified message
console.log('Verified message received:', data);
}
}
Migration Strategies
Strangler Fig Pattern Implementation
// Gradual migration from monolith to micro frontends
class StranglerFigMigration {
private routes: Map<string, 'monolith' | 'microfrontend'> = new Map();
private featureFlags: Map<string, boolean> = new Map();
constructor(private config: MigrationConfig) {
this.initializeRoutes();
}
private initializeRoutes() {
// Start with all routes going to monolith
this.config.allRoutes.forEach(route => {
this.routes.set(route, 'monolith');
});
// Gradually enable micro frontend routes based on feature flags
this.config.microfrontendRoutes.forEach(route => {
const flagName = `enable_mf_${route.replace(/[^a-zA-Z0-9]/g, '_')}`;
if (this.featureFlags.get(flagName)) {
this.routes.set(route, 'microfrontend');
}
});
}
routeRequest(path: string): 'monolith' | 'microfrontend' {
// Check for exact match first
if (this.routes.has(path)) {
return this.routes.get(path)!;
}
// Check for pattern matches
for (const [route, target] of this.routes.entries()) {
if (this.matchesPattern(path, route)) {
return target;
}
}
// Default to monolith for unknown routes
return 'monolith';
}
migrateRoute(route: string) {
console.log(`Migrating route ${route} to micro frontend`);
this.routes.set(route, 'microfrontend');
// Log migration event for monitoring
this.logMigrationEvent(route);
}
rollbackRoute(route: string) {
console.log(`Rolling back route ${route} to monolith`);
this.routes.set(route, 'monolith');
// Log rollback event
this.logRollbackEvent(route);
}
private matchesPattern(path: string, pattern: string): boolean {
// Simple wildcard matching
const regex = new RegExp(
'^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
);
return regex.test(path);
}
private logMigrationEvent(route: string) {
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track('Route Migrated', {
route,
timestamp: Date.now(),
});
}
}
private logRollbackEvent(route: string) {
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track('Route Rolled Back', {
route,
timestamp: Date.now(),
});
}
}
}
interface MigrationConfig {
allRoutes: string[];
microfrontendRoutes: string[];
}
Ecosystem Outlook
The micro frontend ecosystem keeps moving. Three directions are worth tracking:
- Framework-agnostic orchestration such as single-spa, alongside Module Federation
- Edge-side composition, where CDN providers assemble fragments before the response reaches the browser
- Streaming architectures that flush the shell before every fragment has resolved
Where These Patterns Hold
Event sourcing, performance budgeting, and cross-app error tracing pay off once several teams deploy independently against shared state. Below that scale the bookkeeping costs more than the coordination it removes: one team shipping one bundle is better served by build-time integration and a plain store. Add runtime loading only when independent deploys become the constraint. Two costs stay fixed at any scale. Every dynamic load can fail, so graceful degradation belongs in the first release rather than a later hardening pass, and every cross-origin message boundary needs origin checks and signatures from the day it opens.
If you are starting the move, put one monolith route behind the strangler-fig router with a feature flag, so rollback stays a config change.
Complete Series Navigation
- Part 1: Architecture fundamentals
- Part 2: Implementation patterns
- Part 3 (Current): Advanced patterns & debugging
References
- Micro Frontends - Martin Fowler - Foundational article by Cam Jackson defining micro frontend architecture patterns and trade-offs
- micro-frontends.org - Techniques, strategies and recipes for building modern web apps with multiple independent teams
- Module Federation Concepts - webpack - Official webpack documentation on Module Federation for sharing code between micro frontends at runtime
- module-federation.io - Module Federation 2.0 documentation with dynamic type hints, runtime plugin system, and advanced patterns
- single-spa Documentation - Framework for orchestrating multiple micro frontends on a single page
- ModuleFederationPlugin Reference - Full API reference for webpack’s ModuleFederationPlugin configuration options
- Web Vitals - Core Web Vitals definitions and thresholds, the basis for per-micro-frontend performance budgets
Micro Frontend Architecture Guide
A 3-part comprehensive guide to micro frontend architecture, from fundamental concepts to advanced patterns and production debugging strategies.
All Posts in This Series
Related posts
Build the redirect engine, analytics collection, and API Gateway config: performance optimizations and debugging strategies for millions of daily redirects.
Strategies to prevent and handle DynamoDB throttling in Single Table Design: partition key design, write sharding, capacity modes, DAX, and retry patterns.
A comparison of modern TypeScript linting and formatting tools - ESLint, Prettier, Biome, and Oxlint - with benchmarks, config examples, and migration tips.
Run Bun and Deno on AWS Lambda with custom runtimes: performance benchmarks, cost analysis, and production deployment patterns.
Observer, Strategy, Command, State, and Mediator in TypeScript: which ones the language already covers, and when RxJS, Redux, or XState earn their place.