Skip to content

Micro Frontends

advanced22 min read

Why Micro Frontends Exist

Let's get one thing straight right away: micro frontends solve an organizational problem, not a technical one. When a frontend monolith grows past 3-4 teams, merge conflicts, deployment coordination, and shared ownership create friction that slows everyone down. Micro frontends let each team own a vertical slice of the product — from UI to API — and deploy independently.

The idea is borrowed from microservices on the backend. But here's what makes it tricky: the browser is fundamentally different from a server cluster. There is one DOM, one global scope, one URL bar, and one user staring at a single screen. This tension between independent teams and a unified user experience is what makes micro frontends genuinely hard.

Mental Model

Think of a micro frontend architecture like a shopping mall. Each store (team) controls its own interior — layout, inventory, staff. But they all share the mall's corridors (routing), entrance (shell application), and infrastructure (HVAC, electricity). Customers expect a seamless experience walking between stores, even though each store operates independently. The mall management (platform team) provides shared infrastructure without dictating what each store sells.

When Micro Frontends Make Sense

Before you reach for micro frontends, ask yourself honestly: do you actually need them? They introduce real complexity. Use them when:

  • Multiple teams own different parts of the same application (3+ teams is the typical threshold)
  • Independent deployment is a hard requirement — team A cannot wait for team B's release cycle
  • Tech diversity is needed — one team needs React, another has a legacy Angular app that cannot be rewritten overnight
  • Scale — the codebase is so large that a single build takes 10+ minutes, CI is a bottleneck, and developer experience has degraded

If you have one team, or two teams that communicate well, a well-structured monolith with clear module boundaries is almost always better.

Quiz
A company has 2 frontend teams working on the same React app. Builds take 3 minutes. Deploys happen twice a week. Should they adopt micro frontends?

Integration Approaches

Okay, so you've decided you need micro frontends. How do you actually glue them together? There are four primary approaches, and each has very different trade-offs around performance, isolation, and developer experience.

Build-Time Integration

Each micro frontend is published as an npm package. The shell application installs them as dependencies and bundles everything together at build time.

{
  "dependencies": {
    "@acme/header": "^2.1.0",
    "@acme/product-catalog": "^1.8.3",
    "@acme/checkout": "^3.0.1"
  }
}

Pros: Single bundle (no runtime overhead), shared dependencies deduplicated, type checking across boundaries.

Cons: Any change requires rebuilding the shell. Teams cannot deploy independently — the whole point of micro frontends is defeated. This is essentially a monolith with extra steps.

Build-time integration is a stepping stone, not a destination. Use it when migrating from a monolith gradually.

Runtime Integration via JavaScript

The shell application loads micro frontends at runtime as JavaScript bundles. Each micro frontend exposes a mount function that takes a DOM element and renders into it.

// Shell application
async function loadMicroFrontend(name, containerId) {
  const container = document.getElementById(containerId);
  const module = await import(`https://cdn.acme.com/${name}/latest/bundle.js`);
  module.mount(container);

  return () => module.unmount(container);
}

// Micro frontend exposes mount/unmount
export function mount(container) {
  const root = createRoot(container);
  root.render(<ProductCatalog />);
  return root;
}

export function unmount(container) {
  // cleanup
}

Pros: True independent deployment, lazy loading by default, framework-agnostic.

Cons: No shared dependencies by default (each bundle ships its own React), runtime errors instead of compile-time errors, orchestration complexity.

Runtime Integration via iframes

Each micro frontend runs in its own iframe. The simplest form of isolation.

<iframe src="https://catalog.acme.com" title="Product Catalog" />

Pros: Perfect isolation (separate JS context, separate styles, separate crash boundary).

Cons: No shared state without postMessage, deep linking is painful, accessibility issues (screen readers struggle with nested documents), performance overhead of multiple browser contexts, responsive design nightmares.

Server-Side Composition

The server assembles HTML fragments from different micro frontends before sending to the browser.

// Edge function / server middleware
async function composePage(req) {
  const [header, catalog, footer] = await Promise.all([
    fetch('https://header-service.internal/fragment'),
    fetch('https://catalog-service.internal/fragment'),
    fetch('https://footer-service.internal/fragment'),
  ]);

  return `
    <!DOCTYPE html>
    <html>
      <body>
        ${await header.text()}
        ${await catalog.text()}
        ${await footer.text()}
      </body>
    </html>
  `;
}

Pros: Fast initial render (no client-side orchestration), SEO-friendly, works without JavaScript.

Cons: Server-side infrastructure required, interactive parts still need client-side hydration, fragment caching complexity.

Quiz
You need micro frontends where one team uses React and another uses Svelte. Both must share the same page seamlessly. Which integration approach is best?

Routing Ownership

Who owns the URL? This might sound like a simple question, but it's actually the hardest coordination problem in micro frontends.

Option 1: Shell owns all routing. The shell application maps URL patterns to micro frontends. Each micro frontend receives its route segment and handles internal navigation.

/products/*    → Product Catalog MFE
/checkout/*    → Checkout MFE
/account/*     → Account MFE

Option 2: Each micro frontend owns its routes. A shared router dispatches based on the top-level path, and each micro frontend registers its own routes dynamically.

Option 1 is simpler and more predictable. Option 2 allows teams to add routes without coordinating with the shell team, but risks route conflicts.

Common Trap

Deep linking across micro frontends is where routing breaks down. If the checkout MFE needs to link back to a specific product page in the catalog MFE, it must know the catalog's URL structure. This creates implicit coupling. The fix: define a shared route contract — a simple JSON schema that each team publishes declaring their public routes and parameters.

The Shared Dependencies Problem

Here's a problem that'll keep you up at night: if each micro frontend bundles its own React, you ship React 5 times to the user. A 45KB library times 5 is 225KB of duplicated code. Ouch.

Solutions:

  1. Externals map — Declare shared libraries as externals. The shell loads them once, micro frontends reference them from the global scope.

  2. Import maps — Browser-native solution. A JSON manifest maps bare specifiers to URLs.

<script type="importmap">
{
  "imports": {
    "react": "https://cdn.acme.com/shared/react@19.0.0/esm/index.js",
    "react-dom": "https://cdn.acme.com/shared/react-dom@19.0.0/esm/index.js"
  }
}
</script>
  1. Module Federation — Webpack's solution for sharing modules at runtime with version negotiation (covered in the next chapter).
Quiz
Three micro frontends each bundle React 19.0.0. What's the simplest way to deduplicate without changing the build system?

Communication Between Micro Frontends

Micro frontends need to talk to each other. The user adds an item in the catalog MFE, and the header MFE should update the cart count. But direct imports create coupling, which defeats the whole purpose. The goal is loose coupling through well-defined contracts.

Custom Events

The simplest and most framework-agnostic approach:

// Catalog MFE — dispatch
window.dispatchEvent(new CustomEvent('cart:item-added', {
  detail: { productId: '123', quantity: 1 }
}));

// Header MFE — listen
window.addEventListener('cart:item-added', (e) => {
  updateCartCount(e.detail);
});

Shared State Bus

A lightweight pub/sub or observable store that lives in the shell:

// Shared event bus (provided by shell)
const bus = {
  listeners: new Map(),
  emit(event, data) {
    this.listeners.get(event)?.forEach(fn => fn(data));
  },
  on(event, fn) {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set());
    this.listeners.get(event).add(fn);
    return () => this.listeners.get(event).delete(fn);
  }
};
Never share state objects directly

Passing mutable state objects between micro frontends creates invisible coupling. One MFE mutates the object, another reads stale data, and debugging spans two codebases. Always communicate via events with serializable payloads — treat the boundary like a network boundary.

CSS Isolation

Without isolation, one micro frontend's .button class stomps on another's. Three approaches:

  1. Shadow DOM — True style encapsulation. Styles inside a shadow root don't leak out.
  2. CSS Modules / scoped styles — Class names are hashed at build time (button_a1b2c3). No runtime overhead.
  3. Naming conventions — BEM with team prefixes (catalog__button--primary). Low-tech but effective with discipline.

Shadow DOM provides the strongest isolation but makes theming harder — CSS custom properties are the bridge for shared design tokens.

Quiz
Micro frontend A uses Shadow DOM for style isolation. The platform team wants to enforce a shared brand color across all micro frontends. How should this work?

Trade-offs Nobody Tells You About

Let's get real about the costs, because conference talks rarely mention these.

Bundle size grows. Even with shared dependencies, the overhead of shell orchestration, multiple framework instances for polyfills, and duplicated utility code adds up. Expect 15-30% larger total payload compared to a well-optimized monolith.

Consistency suffers. Different teams drift. One team upgrades their design system, another doesn't. Button styles diverge. Spacing is inconsistent. A shared design system with versioned releases helps, but it requires a dedicated platform team to maintain.

Testing gets harder. Integration testing across micro frontends requires running multiple applications simultaneously. End-to-end tests are slow and flaky.

Developer experience degrades. New developers must understand the orchestration layer, shared dependency management, and inter-MFE communication. Local development requires running the shell plus at least one micro frontend.

Key Rules
  1. 1Micro frontends solve organizational scaling problems. If you don't have multiple autonomous teams, you don't need them.
  2. 2Runtime JavaScript integration with mount/unmount contracts is the most flexible approach for heterogeneous tech stacks.
  3. 3Shared dependencies must be deduplicated — import maps or Module Federation prevent shipping React multiple times.
  4. 4Communication between micro frontends should use events or a pub/sub bus with serializable payloads — never share mutable state.
  5. 5CSS isolation via Shadow DOM is strongest, with CSS custom properties as the bridge for shared design tokens.
  6. 6Expect 15-30% bundle size overhead, harder testing, and consistency challenges. Budget for a platform team.
1/8