Shared from "Frontend Concepts Low Level" on Inkdown
Micro frontends extend the microservices philosophy to the browser. Instead of one monolithic frontend app, you have multiple independent frontend apps composed into a single user experience.
Think of a newspaper:
Each section is independent. The sports team doesn't wait for the weather team. They publish on their own schedule. But the reader sees one cohesive newspaper.
A micro frontend architecture consists of:
Each box is a separate app, built, tested, and deployed independently. The shell orchestrates loading and composing them. The user sees a single, unified page.
A large frontend monolith becomes painful when:
| Problem | What Happens |
|---|---|
| Multiple teams, one codebase | Merge conflicts, blocked deploys, "who owns this component?" |
| One tech stack locks everyone | Teams can't choose what works best for their domain |
| Single deploy bottleneck | One team's change blocks everyone else's release |
| Growing build times | 10-minute builds kill productivity |
| Fear of breaking things | Changing a shared component might break 50 pages |
| Big-bang rewrites | "Let's rewrite the whole app" → 2-year project that fails |
Micro frontends are NOT for every project. They solve organizational scaling problems, not technical ones. If you have 2-3 frontend developers on one product, a monolith is fine. If you have 10+ teams building one user-facing product, micro frontends start making sense.
There are fundamentally different ways to compose micro frontends. They range from dead-simple to architecturally complex.
The simplest — just embed apps in iframes.
postMessage needed for communication (clunky, string-only)Legacy integration, embedding third-party content (payment widgets, maps), quick prototypes. Not for serious micro frontend architecture.
Stripe's payment elements used to be iframes. So do many ad embeds. This is fine for isolated widgets but not for building a full page.
Each team publishes their components as npm packages. The host app installs and imports them.
Small-to-medium orgs that want code separation and team ownership but don't need truly independent deployment. This is the most common "starter" approach and honestly the right choice for many teams.
Each micro frontend is loaded at runtime via <script> tags. The shell dynamically loads scripts and mounts apps.
Each micro frontend exposes a global mount/unmount API:
The shell calls these:
Every micro frontend must implement:
Medium orgs that need independent deployment but don't want the complexity of Module Federation. Also used in legacy modernization where you're gradually migrating from monolith.
Each micro frontend is a custom element (Web Component). The shell just uses HTML tags.
React's event system uses synthetic events attached to the root. Shadow DOM creates a separate DOM tree. React's events don't naturally cross the shadow boundary. This means:
onChange, onInput don't fire for shadow DOM inputscomposed: true)ref doesn't work across shadow boundariesYou can work around these, but it's friction.
When you need maximum framework flexibility and teams use different stacks. Works best when:
This is the most sophisticated and widely adopted modern approach. Webpack 5 introduced Module Federation — it lets you configure webpack to expose and consume modules across separate builds at runtime.
This deserves its own deep dive section below.
The composition happens on the server before sending HTML to the browser.
The CDN/edge server (Akamai, Fastly, Cloudflare Workers) fetches each fragment, assembles the page, and sends complete HTML to the browser.
This streams HTML as each section becomes ready. The browser starts rendering immediately.
Content-heavy sites where SEO and initial load performance are critical. E-commerce product pages are the classic use case. Zalando, Amazon, and most large e-commerce sites use server-side composition for their product pages.
Module Federation is the most powerful and widely adopted approach for modern micro frontends. It deserves special attention.
Module Federation lets you configure webpack (or Rspack) so that:
This is the most important part to understand deeply.
shared Configuration — Critical DetailThe shared config is where most bugs happen. Let's break it down:
singleton: trueEnsures only one copy of the module exists in the page. If host has React 18.2 and remote wants React 18.x, they share the host's copy. If singleton: false, each app might load its own copy → two React instances → hooks break.
requiredVersion: '^18'Semver range. If the version available doesn't satisfy this range, webpack loads a separate copy (fallback). This is the safety net.
eager: true vs eager: falseModule Federation isn't just host→remote. Remotes can also consume from the host:
Instead of hardcoding remote URLs at build time, you can load them dynamically:
This lets you change remote URLs without rebuilding the shell — useful for:
Webpack's Module Federation has evolved. The enhanced version adds:
@originjs/vite-plugin-federation — works but not as battle-testedThis is one of the hardest parts. Independent apps need to talk to each other. How?
Pros: Simple, no shared code, browser native, zero dependencies Cons: Hard to debug (events are invisible in component tree), no type safety, event names become implicit contracts, memory leaks if you forget to unsubscribe
All micro frontends read/write to a common store.
| Method | How | Trade-off |
|---|---|---|
| NPM package | Each MFE installs @org/shared-state | Build-time coupling, version management |
| Module Federation shared | Host provides the store instance | Runtime, requires MF config |
| Shell provides via mount props | Shell passes store instance to mount() | Explicit but verbose |
| Global singleton | Store on window.__STORE__ | Quick and dirty, no type safety |
If each MFE bundles its own copy of Zustand/Redux, they each have their own store instance. Updates in one aren't visible in another. You MUST ensure there's exactly one store instance.
Apps communicate through the URL. This is underrated and often the simplest solution.
Pros: Shareable state, works with browser back/forward, SEO-friendly, zero coupling Cons: Limited data capacity (URL length ~2000 chars), serialization overhead, not great for large/complex state
The shell passes callbacks to micro frontends:
Pros: Explicit, type-safe, easy to reason about Cons: Only works for direct parent-child relationships, shell must know about all events, doesn't scale for cross-cutting concerns
For communication between tabs/windows of the same app:
| Pattern | Best For | Scale |
|---|---|---|
| Custom Events / Event Bus | Decoupled communication, any-to-any | Small to large |
| Shared State Store | Complex shared state (cart, auth, user) | Medium to large |
| URL | Filter/search state, bookmarkable state | Any |
| Callback Props | Simple parent-child communication | Small |
| BroadcastChannel | Cross-tab sync | Specialized |
Real-world apps use a combination. URL for navigation/filter state, shared store for cart/auth, event bus for cross-cutting events.
Routing in micro frontends is tricky because you have multiple apps that each might want to control the URL.
Each micro frontend doesn't know about routing — the shell decides what to render where.
Pros: Simple, centralized control, easy to reason about Cons: Shell becomes a bottleneck, must be updated for every new route, teams can't add routes autonomously
Pros: Teams control their own routes, shell is thinner, true autonomy Cons: URL structure must be pre-agreed, deep linking needs coordination, debugging routing issues is harder
Micro frontends register their routes with the shell at runtime:
Pros: Fully dynamic, no shell redeployment needed for new routes, true plug-and-play Cons: Complex, hard to debug routing issues, no static analysis, route conflicts possible
Each team gets a URL prefix. No coordination needed.
The shell is thin — it just matches prefixes and delegates:
Pros: Zero routing conflicts, simple, teams are fully autonomous Cons: URL structure is rigid, can't have routes that span multiple MFes naturally
When using Strategy B or D, only one MFE is "active" at a time. The active MFE's router handles the URL. The inactive ones should not try to read the URL.
One of the biggest practical challenges. If Team A uses Tailwind and Team B uses Bootstrap, styles will collide.
Class names are hashed to unique strings. No collisions possible.
Pros: Simple, zero runtime overhead, great TypeScript support Cons: No style sharing between MFes (each has its own hashed classes)
Pros: Dynamic styles, theming, no class name management Cons: Runtime overhead, larger bundle, SSR complexity, multiple CSS-in-JS libraries = multiple runtime instances
Web Components with Shadow DOM isolate CSS completely:
Trade-offs already covered in the Web Components section.
Pros: Simple, no build tool needed, works everywhere Cons: Relies on discipline, no enforcement, verbose
Instead of fighting CSS, agree on a design system:
Pros: Visual consistency, no CSS collisions, shared language Cons: Design system must be maintained, changes require coordination, some teams may need customizations beyond what the system offers
Combine CSS Modules (for isolation) + Design Tokens (for consistency):
The #1 technical challenge in micro frontends.
| Strategy | Behavior |
|---|---|
| Build-time (npm) | npm/yarn dedupes to one version (usually highest). Team C's React 17 code might break. |
| Module Federation | Shared config negotiates. Team C's requiredVersion: '^17' won't match 18.2 → loads its own copy. |
| Script tag / externals | Each app bundles its own. Multiple instances guaranteed. |
| Iframe | Complete isolation. No conflict possible. |
React, Vue, and most frameworks break with multiple instances.
Each MFE has its own document → its own React instance. No conflict. But you lose all benefits of sharing.
| Dependency | Singleton Required? | Why? |
|---|---|---|
| React | YES | Hooks and Context break with multiple instances |
| React DOM | YES | Tied to React version |
| React Router | YES | Multiple routers fight over URL |
| Redux | Depends | Multiple stores = separate state (might be intentional) |
| Zustand | Depends | Same as Redux |
| styled-components | No | Each instance generates its own classes (but duplicates CSS) |
| Tailwind | No | CSS-only, no runtime |
| Lodash | No | Pure functions, stateless |
| date-fns | No | Pure functions, stateless |
| Axios | No | Each instance works independently |
The original micro frontend framework. Framework-agnostic router that orchestrates mounting/unmounting.
Each MFE must export lifecycle functions:
Pros: Mature, framework-agnostic, handles routing and lifecycle, large ecosystem Cons: Doesn't handle shared dependencies, no built-in module sharing, requires SystemJS for script loading, older architecture
Built on top of single-spa. Adds a feed service for discovering micro frontends at runtime.
How discovery works:
Pros: Full platform with auth, notifications, extensions, tile-based dashboard Cons: Opinionated, heavy, steep learning curve, Piral-specific concepts
Micro frontend framework with web component support and a built-in development console.
Pros: Good for enterprise, iframe-based isolation, SAP ecosystem integration Cons: iframe-based (inherits iframe limitations), SAP-centric
Server-side rendering focused micro frontend framework. Each component is a small Node.js service that renders HTML.
Component-driven development platform. Each component is independently versioned and shared.
Pros: Granular (component-level, not app-level), great DX Cons: More suited for component libraries than full micro frontends
| Feature | single-spa | Piral | Luigi | Module Federation |
|---|---|---|---|---|
| Runtime routing | Yes | Yes | Yes | No (host handles) |
| Shared deps | No | Yes | No | Yes (built-in) |
| Framework agnostic | Yes | Partial | Yes | No (bundler-specific) |
| Discovery | Static config | Feed service | Config file | Static config |
| SSR support | Limited | Yes | Iframe only | With setup |
| Complexity | Medium | High | Medium | Medium-High |
| Maturity | High | Medium | Medium | High |
| Learning curve | Medium | Steep | Low-Medium | Steep |
| Best for | Any stack | Full platform | Enterprise/SAP | React/Vue shops |
remoteEntry.v3.js or hash-based (remoteEntry.a1b2c3.js) for cache bustingHow does the shell know which version of each remote to load?
Each team has their own CI/CD pipeline:
Organization-level checks that run on ALL MFE repos:
Each team tests their MFE in isolation:
Test that the MFE works correctly when loaded by the shell:
Test complete user journeys that span multiple micro frontends:
Test that MFes honor their contracts (mount/unmount API, event bus events, shared state):
| Signal | Description |
|---|---|
| 10+ frontend developers | Multiple teams stepping on each other's code |
| Independent team deployment | Team A can't wait for Team B's release cycle |
| Distinct business domains | Shop, Account, Checkout are clearly separable |
| Long-lived product | You'll maintain this for years, worth the investment |
| Incremental migration needed | Moving from monolith to microservices gradually |
| Different tech needs per domain | Checkout needs high security, Shop needs rich interactivity |
| Signal | Description |
|---|---|
| < 5 frontend developers | Communication is easy, monolith is fine |
| Single product, single team | No organizational scaling problem |
| Tight coupling between features | If everything depends on everything, separation is artificial |
| Short-lived project | Not worth the infrastructure investment |
| Team doesn't have DevOps maturity | Micro frontends need CI/CD, CDN, monitoring |
| "We want to use different frameworks" | Usually not a good enough reason alone |
| Benefit | Cost |
|---|---|
| Independent deployment | More infrastructure (CDNs, config APIs, monitoring) |
| Team autonomy | Coordination overhead (contracts, shared libs, design system) |
| Tech stack freedom | Shared dependency hell, inconsistent UX |
| Fault isolation | More complex error handling, more failure modes |
| Smaller per-team bundles | More HTTP requests, more complex loading |
| Incremental upgrades | Version management across N repos |
Most teams get 80% of the benefit from build-time integration (npm packages) with clear team ownership. Only go to runtime integration (Module Federation) when you truly need independent deployment.
| Criteria | Iframe | NPM Packages | Script Tag | Web Components | Module Federation | Server-Side |
|---|---|---|---|---|---|---|
| Independent Deploy | Yes | No | Yes | Yes | Yes | Yes |
| JS Isolation | Perfect | None | None | Shadow DOM | Shared scope | N/A |
| CSS Isolation | Perfect | None | None | Shadow DOM | Manual | Manual |
| Shared Deps | No | Yes (deduped) | Manual | No | Automatic | N/A |
| TypeScript | No | Full | No | Limited | Good | N/A |
| SSR | No | Yes | No | Partial | Complex | Yes (native) |
| SEO | Bad | Good | Good | Good | Good | Best |
| Performance | Worst | Best (single bundle) | Good | Good | Good | Best (SSR) |
| DX | Bad | Best | Medium | Medium-Hard | Medium-Hard | Medium |
| Complexity | Low | Low | Medium | Medium | High | High |
| Framework Lock | None | Same framework | Same framework | None | Same framework | Any |
| Maturity | Ancient | Standard | Standard | Standard | Modern | Standard |
| Best For | 3rd party embeds | Small orgs | Medium orgs | Mixed stacks | Large orgs | Content sites |
Micro frontends solve organizational problems, not technical ones. If your team is small, use a monolith.
Start with build-time integration (npm packages). Only move to runtime integration when you truly need independent deployment.
Module Federation is the modern standard for runtime micro frontends in React/Vue ecosystems. Understand the shared config deeply.
Communication is the hardest part. Use a typed event bus for decoupled communication and a shared store for complex shared state.
CSS isolation matters. CSS Modules + Design Tokens is the pragmatic sweet spot.
Shared dependencies must be managed carefully. Singleton frameworks (React, Vue) MUST have only one instance.
Deployment independence is the whole point. If you can't deploy independently, you don't have micro frontends — you have a distributed monolith.
Don't mix frameworks unless you have a very good reason. The "framework freedom" promise sounds great but creates enormous operational overhead.
Server-side composition is best for SEO/performance. Client-side composition is best for interactivity. Many production apps use both.
The shell should be thin. If the shell grows large, you've just rebuilt the monolith with extra steps.