Microfrontend Architecture
Microfrontend is an architectural approach where a frontend application is split into multiple independent applications (micro-apps), each responsible for its own part of functionality. These applications can be developed, tested, and deployed independently of each other, but ultimately are combined into a single user interface.
Key Idea:
Microfrontend is the application of microservices architecture principles to frontend development. Each team can work on their part of the application autonomously, using their own tech stack and releasing updates independently.
Core Concepts
Independent Applications
Each microfrontend is a standalone application with its own business logic, styles, and dependencies. It can be written in any framework (React, Vue, Angular, etc.).
Isolation
Microfrontends are isolated from each other: changes in one should not break the work of others. This is achieved through style encapsulation mechanisms, using Shadow DOM or CSS Modules.
Composition
There is a shell application (host, container) that combines all microfrontends into a single interface. It manages routing and determines which microfrontend to show on a specific route.
Independent Deployment
Each microfrontend can be updated and deployed separately without affecting other parts of the system. This speeds up the development cycle and reduces risks.
Implementation Approaches
Server-Side Composition (SSI)
Microfrontends are assembled on the server side using technologies like Server-Side Includes (SSI), Edge-Side Includes (ESI), or templating engines.
Pros:
- Simple implementation
- Good performance (client receives ready page)
Cons:
- Complex interaction between microfrontends on client side
- Less flexibility for dynamic updates
Build-Time Integration
Microfrontends are published as npm packages and included in the main application at build time.
Pros:
- Easy integration
- Optimized build
Cons:
- Loss of deployment independence (need to rebuild entire application)
- Cannot update one microfrontend without rebuilding
Run-Time Integration via iframes
Each microfrontend loads in a separate iframe.
Pros:
- Complete isolation (styles, JavaScript, global variables)
- Simple integration
Cons:
- Performance issues
- Difficulties with communication and routing
- Poor accessibility (a11y)
Run-Time Integration via JavaScript (Module Federation)
Most modern approach: microfrontends are loaded dynamically at runtime via Webpack Module Federation or similar tools.
Pros:
- Independent deployment
- Shared dependencies (react, react-dom) load once
- Flexible composition
Cons:
- Complex setup
- Requires careful dependency version management
Web Components
Microfrontends are implemented as Custom Elements (Web Components).
Pros:
- Native browser support
- Encapsulation via Shadow DOM
Cons:
- Limited support in older browsers
- Integration complexity with some frameworks
Module Federation Example Structure
Microfrontend Advantages
-
Team Independence
Each team works on their microfrontend autonomously without blocking others. This speeds up development and reduces dependencies. -
Technology Diversity
Different microfrontends can use different frameworks and libraries. For example, one in React, another in Vue, third in Angular. -
Independent Deployment
Updating one microfrontend doesn't require redeploying the entire application. This reduces risks and speeds up new feature delivery. -
Development Scalability
A large project can be divided between multiple teams, each focusing on their business domain. -
Simplified Migration
You can gradually migrate parts of a monolith to new technologies without rewriting the entire application at once.
Disadvantages and Challenges
-
Infrastructure Complexity
Need to set up build, CI/CD, monitoring, and logging for each microfrontend. -
Increased Application Size
Without using shared dependencies, the same libraries (React, lodash) may load multiple times. -
Communication Complexity
Microfrontends must somehow exchange data and events. Need a well-thought-out mechanism (e.g., event bus, shared state). -
Versioning Issues
Different microfrontends may use different versions of the same library, leading to conflicts. -
Code Duplication
Common UI components and utilities need to be either extracted into separate packages or duplicated in each microfrontend. -
Testing Complexity
E2E tests must account for interaction of all microfrontends. Integration tests become more complex.
When to Use Microfrontends
Warning:
Microfrontends are not a silver bullet. They add complexity and are not suitable for all projects.
Suitable for:
- Large applications with multiple teams
- Projects requiring deployment independence
- Gradual migration from legacy code to new technologies
- Complex enterprise systems with different business domains
Not suitable for:
- Small projects or MVPs
- Teams of 1-3 developers
- Projects with simple structure where modular architecture is sufficient
Best Practices
Define Boundaries
Divide microfrontends by business domains (products, checkout, user-profile), not by technical layers.
Use Shared Dependencies
Configure shared dependencies in Module Federation to avoid library duplication.
Create a Design System
Common UI components (buttons, inputs, modals) should be in a separate library used by all microfrontends.
Plan Communication
Use an event bus (e.g., based on CustomEvents) or shared state (Redux, Zustand) for data exchange between microfrontends.
Monitoring and Logging
Set up centralized monitoring (Sentry, DataDog) to track errors across all microfrontends.
Versioning Agreements
Agree on dependency update policy and use semver for compatibility control.
Communication Example Between Microfrontends
// Shared Event Bus (shared library)
class EventBus {
private events: Map<string, Function[]> = new Map();
emit(event: string, data?: any) {
const handlers = this.events.get(event) || [];
handlers.forEach(handler => handler(data));
}
on(event: string, handler: Function) {
const handlers = this.events.get(event) || [];
handlers.push(handler);
this.events.set(event, handlers);
}
off(event: string, handler: Function) {
const handlers = this.events.get(event) || [];
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
this.events.set(event, handlers);
}
}
export const eventBus = new EventBus();
// In Products microfrontend
import { eventBus } from '@shared/event-bus';
function addToCart(product: Product) {
eventBus.emit('cart:add', product);
}
// In Cart microfrontend
import { eventBus } from '@shared/event-bus';
useEffect(() => {
const handler = (product: Product) => {
setCartItems([...cartItems, product]);
};
eventBus.on('cart:add', handler);
return () => {
eventBus.off('cart:add', handler);
};
}, [cartItems]);
Alternatives to Microfrontends
Before implementing microfrontends, consider simpler approaches:
- FSD (Feature-Sliced Design) — for structuring a monolith
- Modular Architecture — for splitting into independent modules within one application
- Monorepo with shared packages — for code reuse between projects
Summary:
Microfrontends are a powerful tool for scaling large teams and complex applications. They allow teams to work independently and deploy changes autonomously. However, they add significant complexity to infrastructure, testing, and development. Use microfrontends only when their benefits (team independence, technology diversity) outweigh the costs of implementation and maintenance.