Skip to main content

Micro-Frontends

TL;DR​

Micro-frontends decompose large UI into independently owned and deployable modules by different teams. Approaches include iframes (simple isolation, performance cost), Module Federation (webpack-native, shared dependencies), and Web Components (standards-based, framework-agnostic). Trade-off: added complexity buys team autonomy and deployment independence at scale.

Learning Objectives​

You will be able to:

  • Evaluate and choose between iframes, Module Federation, and Web Components based on team structure and performance needs.
  • Implement shared dependency management across micro-frontend boundaries.
  • Plan independent versioning, API contracts, and deployment strategies.
  • Monitor cross-module communication and identify integration bottlenecks.

Motivating Scenario​

Your company operates an e-commerce platform with 50+ frontend engineers split across checkout, product discovery, recommendations, and payment teams. Deployments require coordination across all teams—a single bug in recommendations blocks checkout releases. Teams want autonomy: checkout wants React 18, recommendations prefers Vue 3. The platform needs independent deployments where payment team can ship a critical fix without waiting for the product team's review cycle.

Micro-frontends solve this: each team owns a vertical slice of UI, deploys independently, and integrates at runtime. Checkout runs on React, recommendations on Vue, and they coexist in a unified shell application.

Core Concepts​

What Are Micro-Frontends?​

Micro-frontends extend the microservices philosophy to frontend layers. Instead of a monolithic frontend codebase deployed as one artifact, you split the UI into decoupled, independently deployable modules. Each module:

  • Is owned by a single team or squad
  • Has its own repository, build process, and deployment pipeline
  • Communicates through well-defined APIs and events
  • Can use different frameworks, libraries, or versions

This approach scales to large organizations where frontend monoliths become bottlenecks.

Why Consider Micro-Frontends?​

Team Autonomy: Teams deploy without coordinating with others. Removes release blockers.

Technology Flexibility: Different teams can use different frameworks. No forced standardization.

Independent Scaling: High-traffic modules (checkout) can optimize independently from lower-priority ones (help text).

Fault Isolation: A bug in recommendations doesn't crash the entire page.

Faster Development: Smaller codebases are easier to understand and modify.

Org Structure Alignment: Conway's Law: system architecture mirrors communication structure. Micro-frontends align code ownership with team structure.

When Micro-Frontends Are Overkill​

For small teams (<10 engineers) or simple applications, a monolithic frontend is simpler and faster. Micro-frontends introduce:

  • Build complexity (multiple entry points, shared dependency resolution)
  • Runtime overhead (network requests to load modules, duplicate dependencies)
  • Testing complexity (integration testing across module boundaries)
  • Operational overhead (monitoring, versioning, rollback)

Only adopt if the pain of the monolith (coordination overhead, slow builds) exceeds the complexity burden.

Integration Approaches​

1. Iframes​

Embed micro-frontends as iframes in a shell (host) application.

Pros:

  • Strongest isolation: each iframe has its own DOM, JavaScript context, and style scope
  • Simple integration: load URL, embed
  • No shared dependencies (each iframe loads its own React, CSS)
  • Safe to run third-party code

Cons:

  • Performance: each iframe is a full browser context (memory overhead, slower startup)
  • Communication: must use postMessage API (verbose, cross-context)
  • Styling: difficult to theme consistently across iframes (duplication)
  • SEO: search engines may not index iframe content
  • Responsive design: iframes complicate responsive layouts

Use iframe when:

  • Integration with third-party widgets (ads, chat)
  • Strong isolation requirements (untrusted code)
  • Modules are rarely updated (static content)
  • Performance is less critical than isolation
shell/App.jsx

export default function ShellApp() {
const [checkoutVisible, setCheckoutVisible] = useState(false);

return (
<div>
<nav>Home | <button onClick={() => setCheckoutVisible(true)}>Checkout</button></nav>
{checkoutVisible && (
<iframe
title="Checkout Module"
src="https://checkout-service.example.com"
style={{
width: '100%',
height: '600px',
border: 'none',
}}
/>
)}
</div>
);
}

2. Module Federation (Webpack)​

Webpack plugin that allows dynamic loading of shared dependencies at runtime.

Pros:

  • Shared dependencies: load React once, shared across all modules (reduces bundle size)
  • Framework-native: feels integrated, not bolted-on
  • Versioning: can specify min/max versions (Angular 13+)
  • Build-time type safety: TypeScript support for shared types
  • Dynamic loading: modules loaded on-demand

Cons:

  • Webpack-specific: tied to webpack build tool
  • Version conflicts: shared dependencies can have subtle version mismatches (React.useState() from different versions)
  • Debugging: stack traces cross module boundaries (harder to trace)
  • CSS isolation: no automatic scoping (global CSS conflicts)
  • Learning curve: mental model of federated modules is complex

Use Module Federation when:

  • Large teams with heavy build systems (webpack already in use)
  • Shared dependencies matter (React, state management)
  • Frequent module updates
  • Performance optimization of bundle size is critical
shell/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
mode: 'production',
entry: './src/index',
output: {
path: __dirname + '/dist',
filename: '[name].[contenthash].js',
},
devServer: {
port: 3000,
historyApiFallback: true,
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
filename: 'remoteEntry.js',
exposes: {
'./store': './src/store',
},
remotes: {
checkout: 'checkout@http://localhost:3001/remoteEntry.js',
recommendations: 'recommendations@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, strictVersion: false },
'react-dom': { singleton: true, strictVersion: false },
zustand: { singleton: true },
},
}),
],
};

3. Web Components​

Use native Web Components (Custom Elements + Shadow DOM) as the integration boundary.

Pros:

  • Framework-agnostic: Web Components work with React, Vue, Angular, vanilla JS
  • Standard API: no build-time tooling required
  • Shadow DOM: built-in style isolation
  • Easy composition: &lt;checkout-app&gt;&lt;/checkout-app&gt; in HTML
  • Long-term stability: web standard, not dependent on framework

Cons:

  • Limited feature set: no built-in data binding or reactivity
  • Browser support: need polyfills for older browsers
  • Attributes-only API: passing complex objects requires serialization
  • Debugging: Shadow DOM debugging less intuitive
  • Framework integration: some frameworks have limited Web Component support

Use Web Components when:

  • Framework-agnostic integration essential
  • Modules are semi-independent (rarely update together)
  • Building design system with multi-framework support
  • Long-term maintainability > short-term velocity
checkout/CheckoutWebComponent.js
class CheckoutApp extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.render();
}

render() {
const template = `
<style>
:host {
display: block;
font-family: system-ui, -apple-system, sans-serif;
}
.form {
padding: 20px;
border: 1px solid #ccc;
}
</style>
<div class="form">
<h2>Checkout</h2>
<button id="submit">Submit Order</button>
</div>
`;

this.shadowRoot.innerHTML = template;
this.shadowRoot
.getElementById('submit')
.addEventListener('click', () => this.dispatchEvent(
new CustomEvent('checkout-complete', {
detail: { orderId: Math.random() },
bubbles: true,
})
));
}
}

customElements.define('checkout-app', CheckoutApp);

Shared Dependencies & Versioning​

Dependency Management​

Critical: shared dependencies (React, routing, state management) must be coordinated.

Options:

  1. Singleton Pattern: Load dependency once, share across modules. Risk: version mismatch causes subtle bugs (e.g., different React hooks implementations).
  2. Lock Versions: All modules use identical versions. Trade-off: blocks framework upgrades.
  3. Version Ranges: Specify compatible ranges (React 17-18, NOT React 15). Webpack Federation supports singleton: true, strictVersion: false.

Best practice: Use semver with clear upgrade paths. Avoid major version skew in shared dependencies.

API Contracts​

Define clear contracts between modules:

shell/types/checkout.ts
export interface CheckoutAPI {
initialize(config: CheckoutConfig): Promise<void>;
placeOrder(items: CartItem[]): Promise<OrderResult>;
onError: (error: CheckoutError) => void;
}

export interface CheckoutConfig {
apiUrl: string;
userId: string;
currencyCode: string;
}

export interface CartItem {
id: string;
quantity: number;
price: number;
}

export interface OrderResult {
orderId: string;
timestamp: number;
}

Patterns & Pitfalls​

Pattern: Event Bus​

Use a shared event bus for inter-module communication instead of direct coupling.

Problem: Checkout module directly calls recommendations module's API → tight coupling.

Solution: Publish events; modules subscribe.

event-bus.js
class EventBus {
private listeners = new Map();

on(event, handler) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(handler);
}

emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(h => h(data));
}
}
}

// Shell app
const eventBus = new EventBus();

// Checkout module: publishes event
eventBus.emit('order:placed', { orderId: '123', items: [...] });

// Recommendations module: listens
eventBus.on('order:placed', (data) => {
console.log('Update recommendations after order:', data.orderId);
});

Pitfall: Dependency Hell​

Problem: Versions of React diverge between modules. Checkout uses React 18 hooks, recommendations uses React 17 context. Different React instances lead to lost state, duplicate components in DOM.

Mitigation:

  • Enforce singleton loading via Module Federation
  • Lock major versions in shared dependencies
  • Test with multiple versions in CI

Pitfall: Global CSS Conflicts​

Problem: Checkout defines button { padding: 10px }, recommendations defines button { padding: 20px }. Modules' styles fight.

Mitigation:

  • Use CSS-in-JS (styled-components, CSS Modules) with scoped selectors
  • Use Shadow DOM (Web Components) for strong isolation
  • BEM naming convention for traditional CSS

Operational Considerations​

Performance Budgets​

Load time: Each module adds HTTP requests. Monitor:

  • remoteEntry.js size (shared deps manifest)
  • Module chunk sizes (lazy loading)
  • Total time to interactive

Typical overhead:

  • iframes: +100-300ms per iframe (separate browser context)
  • Module Federation: +50-100ms (dynamic import, shared dep resolution)
  • Web Components: +10-30ms (custom element registration)

Monitoring & Error Tracking​

Track cross-module failures:

error-tracking.js
window.addEventListener('error', (event) => {
if (event.filename.includes('remoteEntry.js')) {
// Module federation failure
reportError({
type: 'MODULE_LOAD_FAILURE',
module: extractModuleName(event.filename),
message: event.message,
timestamp: Date.now(),
});
}
});

// Custom event for checkout-specific errors
window.addEventListener('checkout-error', (event) => {
reportError({
type: 'CHECKOUT_MODULE_ERROR',
...event.detail,
});
});

Versioning Strategy​

  • Module version: Track independently. Use semver.
  • API version: Define major/minor for public contracts.
  • Shared dependency version: Lock major versions; test minor upgrades in canary.

Example version matrix:

Checkout v2.1.0  →  API v2  →  Requires: React 18.0+, Zustand ^4.0
Recommendations v1.5.2 → API v1 → Requires: React 17+, Zustand ^3.5

Design Review Checklist​

  • Is module ownership clear (one team per module)?
  • Are API contracts documented (TypeScript interfaces)?
  • Is independent deployability tested in CI/CD?
  • Are shared dependencies versioned and documented?
  • Is error isolation verified (module crash doesn't crash shell)?
  • Are integration tests for module boundaries in place?
  • Is monitoring configured for cross-module failures?
  • Can modules be lazy-loaded to reduce initial bundle?
  • Is CSS/style isolation strategy implemented?
  • Are rollback procedures for module versions documented?

When to Use / When Not to Use​

Use Micro-Frontends When:

  • Multiple teams (5+) with conflicting release cycles
  • Independent technology choices required per team
  • Large frontend codebase with coordination overhead
  • Different modules have vastly different performance/scale needs
  • Want to adopt new framework versions incrementally

Avoid Micro-Frontends When:

  • Single team or tight-knit squad
  • High coupling between features (frequent inter-module changes)
  • Performance is already a primary concern (micro-frontends add complexity)
  • Simple application (startup MVP, internal tool)
  • Team lacks operational maturity (weak monitoring, deployment pipelines)

Showcase: Comparison of Approaches​

Self-Check​

  1. At what team size do micro-frontends become valuable? Why doesn't a 5-engineer startup need them?
  2. What is the key difference between Module Federation's singleton pattern and version locking? Which is more flexible?
  3. How would you prevent a crash in the recommendations module from taking down the entire page?

Next Steps​

One Takeaway​

info

Micro-frontends solve organizational scaling problems, not technical ones. Start with a monolith; split only when coordination overhead becomes the bottleneck. When you do, begin with iframes for simplicity, graduate to Module Federation for performance.

References​

  1. Webpack Module Federation Documentation
  2. Micro Frontends - Martin Fowler
  3. single-spa: JavaScript Framework for Micro Frontends
  4. Web Components Custom Elements Standard
  5. Micro Frontends Architecture Best Practices