Micro-Frontends – Scalable Web Architecture for Large Teams in 2026
What Are Micro-Frontends?
Micro-frontends extend the microservices philosophy to the frontend layer. Instead of building and deploying one monolithic single-page application, you split the user interface into smaller, self-contained applications — each owned by a different team, each with its own repository, build pipeline, and deployment schedule. The user sees a seamless product; behind the scenes, multiple independent units collaborate to compose the page.
The concept emerged around 2016 from organisations like Zalando and IKEA that were scaling backend microservices but found their frontend remained a bottleneck. A single React or Angular monolith meant that all feature teams had to coordinate releases, shared global state created coupling, and a bug in one team's code could bring down the entire application. Micro-frontends solve this by giving each team end-to-end ownership from the database to the UI component.
By 2026, micro-frontends have matured from an experimental pattern into a mainstream architecture choice supported by robust tooling — Module Federation, single-spa, Piral, Native Federation, and build-time composition strategies. However, this architecture is not free: it introduces complexity in routing, shared state, consistent styling, and performance optimisation. Understanding when and how to adopt it is critical.
Why Micro-Frontends? The Problem with Monoliths
A monolithic frontend works well for small teams and early-stage products. But as the organisation grows, several pain points emerge:
Deployment Coupling
In a monolith, every feature lives in the same codebase. If Team A's checkout flow is ready to ship but Team B's product page has a blocking bug, both wait. Release trains become slow and risky because they bundle unrelated changes together.
Build Time
A large React or Next.js application can take 10–30 minutes to build. Every PR triggers a full rebuild and test suite. CI costs rise, developer feedback loops lengthen, and engineers spend more time waiting than coding.
Codebase Complexity
Thousands of components, hundreds of shared utilities, tangled import graphs — the monolith becomes a maze. Onboarding new developers takes weeks. Refactoring is risky because changes to shared code have unpredictable downstream effects.
Technology Lock-in
Migrating from Angular to React (or React to something newer) in a monolith is an all-or-nothing proposition. With micro-frontends, you can migrate incrementally — one section at a time — running old and new frameworks side by side.
Team Autonomy
Micro-frontends let each team choose the tools and release cadence that suits their domain. The checkout team can use React with a weekly release cycle while the analytics dashboard team uses Svelte with continuous deployment. Autonomy increases ownership and velocity.
Implementation Strategies
There is no single way to build micro-frontends. The right approach depends on your runtime requirements, team structure, and performance budget.
Build-Time Integration
Each micro-frontend is published as an npm package. A shell application imports them at build time and produces a single optimised bundle.
Pros: Simple mental model, single deployment artifact, standard bundler optimisations (tree shaking, code splitting).
Cons: Teams still need to coordinate releases because the shell rebuilds whenever a dependency updates. Not truly independent deployment.
{
"dependencies": {
"@myorg/checkout-mfe": "^2.1.0",
"@myorg/product-mfe": "^3.4.0",
"@myorg/search-mfe": "^1.8.0"
}
}
Iframes
Each micro-frontend runs inside its own <iframe>. Complete isolation is guaranteed — separate DOM, CSS, and JavaScript context.
Pros: Strongest isolation, impossible for one MFE to break another, any framework can be used.
Cons: Poor UX (no shared scroll, accessibility challenges, no seamless navigation), performance overhead from loading separate HTML documents, difficult cross-frame communication.
Iframes are rarely the primary strategy in 2026 but remain useful for embedding third-party widgets or legacy applications that cannot be otherwise integrated.
Web Components
Each micro-frontend exposes its root as a Custom Element. The shell application renders <checkout-mfe></checkout-mfe> in the page, and the browser's Shadow DOM provides style encapsulation.
Pros: Framework-agnostic, standard browser API, good style isolation via Shadow DOM.
Cons: Shadow DOM complicates global theming, hydration for SSR is non-trivial, event bubbling across shadow boundaries requires care.
class CheckoutMFE extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
const root = document.createElement('div');
shadow.appendChild(root);
ReactDOM.createRoot(root).render(<CheckoutApp />);
}
}
customElements.define('checkout-mfe', CheckoutMFE);
Module Federation (Runtime Integration)
Module Federation is the dominant approach in 2026. Introduced in webpack 5, it allows separate builds to expose and consume modules at runtime. A host application declares remote entry points; when the user navigates to a section, the browser fetches the remote's JavaScript bundle and mounts its components.
Pros: True independent deployment, runtime composition, shared dependencies to avoid duplication, works with React, Vue, Angular, Svelte.
Cons: Configuration complexity, version skew between shared libraries, runtime errors if a remote is unavailable.
Module Federation Deep Dive
Module Federation treats each micro-frontend as a container that can both expose modules and consume modules from other containers. The configuration lives in the bundler config.
Host Configuration (Shell App)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
product: 'product@https://product.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
Remote Configuration (Checkout MFE)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: {
'./CheckoutApp': './src/CheckoutApp',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
Loading a Remote Component
const CheckoutApp = React.lazy(() => import('checkout/CheckoutApp'));
function App() {
return (
<React.Suspense fallback={<div>Loading checkout...</div>}>
<CheckoutApp />
</React.Suspense>
);
}
Native Federation and Vite
For projects using Vite or Rspack, the Native Federation library (@softarc/native-federation) brings Module Federation semantics without webpack. The Vite plugin for Module Federation (@module-federation/vite) is also actively maintained. In 2026, both tools are production-ready and offer faster build times than webpack for most projects.
Routing and Communication Patterns
Routing
The shell application owns top-level routes. Each route maps to a micro-frontend:
/checkout/* → Checkout MFE
/products/* → Product MFE
/dashboard/* → Dashboard MFE
The shell uses a client-side router (React Router, Vue Router) to mount the correct remote component. Within its route prefix, each MFE manages its own sub-routes independently.
For server-side composition, tools like Podium (from Finn.no) or edge-side includes (ESI) assemble HTML fragments from different services before sending the page to the browser. This approach delivers faster Time to First Byte and better SEO.
Communication
Micro-frontends should communicate through loosely coupled mechanisms:
- Custom Events —
window.dispatchEvent(new CustomEvent('cart-updated', { detail: { itemCount: 3 } })). Other MFEs listen for this event. - URL state — query parameters and hash fragments as a shared contract.
- Shared store — a lightweight pub/sub or a small Redux/Zustand store loaded once by the shell and injected into each MFE.
- Props down, events up — the shell passes data to MFEs via component props; MFEs emit events to notify the shell of changes.
The golden rule: micro-frontends should never import code directly from another micro-frontend. All shared code belongs in a published package or is injected by the shell.
Shared State and Styling
State Management
Sharing global state (authenticated user, locale, theme) is necessary but must be carefully scoped. The shell maintains the global state and passes it via context or props. Individual MFEs manage their own local state independently.
A pattern that works well is an event-driven state bus:
const bus = new EventTarget();
// Shell sets user
bus.dispatchEvent(new CustomEvent('auth', { detail: { user } }));
// MFE reads user
bus.addEventListener('auth', (e) => setUser(e.detail.user));
Styling
CSS conflicts are the most common micro-frontend headache. Strategies include:
- CSS Modules or CSS-in-JS — scoped class names prevent collisions.
- Shadow DOM — strongest encapsulation but limits global theme access.
- BEM or naming conventions — each MFE prefixes its classes (e.g.,
checkout__button,product__card). - Design tokens — a shared package distributes CSS custom properties (
--color-primary,--spacing-md). Each MFE consumes tokens but defines its own component styles.
The design system should live in a shared library that all MFEs depend on. This ensures visual consistency while allowing independent development.
Testing Micro-Frontends
Testing becomes more nuanced in a micro-frontend architecture:
Unit Tests
Each MFE runs its own unit tests in isolation using Jest, Vitest, or Testing Library. This is no different from testing a normal application.
Integration Tests
Test the MFE in the context of the shell. Use Cypress or Playwright to navigate to the route that loads the MFE and verify that data flows correctly across the boundary.
Contract Tests
Define a contract (expected props, events, API endpoints) between the shell and each MFE. Tools like Pact can automate contract verification, ensuring that a new MFE version does not break the shell's expectations.
End-to-End Tests
Run E2E tests against the fully composed application. Since MFEs are deployed independently, your E2E suite should test production-like URLs where all remotes are resolved.
test('user can complete checkout', async ({ page }) => {
await page.goto('https://staging.example.com/products/1');
await page.click('[data-testid="add-to-cart"]');
await page.goto('https://staging.example.com/checkout');
await page.fill('[data-testid="card-number"]', '4242424242424242');
await page.click('[data-testid="pay-button"]');
await expect(page.locator('.order-confirmation')).toBeVisible();
});
Real-World Examples
IKEA
IKEA's website serves millions of users across dozens of markets. Their frontend is composed of micro-frontends, each handling a different domain: product listing, cart, checkout, store finder. Teams in different countries can localise and deploy their section independently without coordinating global releases.
Spotify
Spotify's desktop application uses a micro-frontend-like architecture where individual UI sections (playlists, search, now-playing, library) are developed by separate teams. Each section is an iframe-based module that communicates through a well-defined API layer.
Zalando
Zalando, one of the pioneers of micro-frontends, split their fashion e-commerce platform into team-owned fragments. They built Project Mosaic, an open-source composition framework, to assemble page layouts from independently deployed services.
SAP
SAP's Luigi framework enables micro-frontend composition for enterprise applications. It provides a shell with configurable navigation, authentication, and localisation, while feature teams deliver individual micro-frontends.
Pros and Cons Summary
Advantages
- Independent deployments — ship features without waiting for other teams.
- Team autonomy — each team owns their domain end-to-end.
- Incremental migration — adopt new frameworks one section at a time.
- Fault isolation — a crash in one MFE does not bring down the whole page.
- Smaller codebases — each MFE is easier to understand, test, and maintain.
- Faster CI/CD — builds are scoped to individual MFEs, reducing pipeline duration.
Disadvantages
- Operational complexity — more repositories, more pipelines, more infrastructure.
- Performance risk — duplicate frameworks, extra network requests, increased bundle size.
- Consistent UX — harder to maintain a unified design without a strong design system.
- Cross-cutting concerns — authentication, analytics, error tracking must be coordinated.
- Developer experience — running multiple MFEs locally for development can be cumbersome.
- Debugging — tracing issues across MFE boundaries is more difficult than in a monolith.
When NOT to Use Micro-Frontends
Micro-frontends are not a silver bullet. Avoid them when:
- You have a small team (fewer than 3–4 frontend developers). The overhead of multiple repositories and deployment pipelines outweighs the benefits.
- The product is simple — a marketing site, a blog, or a single-purpose tool does not need architectural decomposition.
- You lack platform engineering capacity — micro-frontends require shared infrastructure: a shell app, a design system, CI/CD templates, monitoring. Without dedicated platform support, teams will reinvent the wheel.
- Performance is the top priority — a well-optimised monolith will always outperform a poorly managed micro-frontend setup. If your user base is on slow networks or low-end devices, every extra kilobyte of framework duplication matters.
The best advice: start with a well-structured monolith. Split into micro-frontends only when organisational scaling pain becomes the dominant bottleneck. Premature decomposition creates complexity without delivering proportional value.
Conclusion
Micro-frontends offer a powerful architectural pattern for organisations where multiple teams need to develop, test, and deploy frontend features independently. In 2026, tools like Module Federation, Native Federation, and frameworks like single-spa and Piral have made the pattern accessible and production-proven. However, the complexity cost is real — duplicate bundles, routing coordination, shared state, and consistent styling all require deliberate engineering.
The decision to adopt micro-frontends should be driven by team structure and deployment needs, not by technical novelty. If your organisation has outgrown the monolith and independent deployment is a genuine requirement, micro-frontends are a proven solution. If your team is small and ships comfortably from a single codebase, keep it simple.
Need help? Contact us.

