Suggest an editImprove this articleRefine the answer for “Microfrontend architecture”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Microfrontend architecture** splits a frontend app into independent deployable micro-apps, each owned by a separate team, that compose into one UI at runtime. The main tools are Module Federation (Webpack 5) and single-spa. Apply it when 3+ teams need independent deployments. Skip it for teams under 20 developers.Shown above the full answer for quick recall.Answer (EN)Image**Microfrontend architecture** splits a frontend application into multiple independent web apps, each owned by a separate team, that load and compose into a single UI at runtime. ## Theory ### TL;DR - Analogy: like Lego blocks - each team builds and ships their own block independently; the shell app snaps them together at runtime without rebuilding the whole set - Main difference vs monolith: teams deploy separately, pick their own frameworks (React for team A, Vue for team B), and one team's broken CSS cannot take down another team's UI - Module Federation (Webpack 5) is the dominant approach in production today; single-spa is the other popular choice - Use if you have 50+ devs across multiple teams; skip it for teams under 20 - The real cost is infrastructure: every MF needs its own CI/CD, monitoring, and versioning ### Quick example Shell app using single-spa loads two microfrontends at runtime: ```html <!-- index.html - shell loads MF1 (React) and MF2 (Vue) --> <!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/single-spa@latest/lib/system/single-spa.min.js"></script> </head> <body> <div id="navbar"></div> <!-- React MF mounts here --> <div id="dashboard"></div> <!-- Vue MF mounts here --> <script> // Register navbar MF (React, always active) System.import('http://localhost:3001/navbar.js').then(navbar => singleSpa.registerApplication('navbar', navbar.default, () => true)); // Register dashboard MF (Vue, active only on /dash routes) System.import('http://localhost:3002/dashboard.js').then(dashboard => singleSpa.registerApplication('dashboard', dashboard.default, () => window.location.pathname.startsWith('/dash'))); singleSpa.start(); </script> </body> </html> ``` The shell fetches each MF bundle from its own server. Navigate to `/dash` and single-spa mounts the Vue app in `#dashboard`. Navigate away and it unmounts. The React navbar stays up the whole time. ### Key difference vs monolith In a monolith, all teams share one repo, one build, and one deploy. Team A's bad CSS cascades into Team B's buttons. Team C's dependency upgrade breaks Team D's tests. Nobody ships on Friday. Microfrontends break that coupling at the infrastructure level: each app has its own bundle, its own release cadence, and its own container registry entry. Team A ships React 18 without waiting for Team B to finish their Angular 16 migration. ### Integration approaches There are four main approaches, each with different tradeoffs. **Run-time via Module Federation (Webpack 5)** is the current production standard. The shell's `webpack.config.js` declares remote MFs by URL; each remote exposes components. Shared dependencies (React, react-dom) load once as singletons. Teams deploy independently and the shell picks up changes on next page load. **Run-time via single-spa** is a framework-agnostic orchestrator. Each MF registers a lifecycle (bootstrap/mount/unmount) and an activity function that tells the orchestrator when to show it. Works well for heterogeneous stacks. **iframes** give complete isolation and are simple to set up, but have terrible ergonomics. Cross-origin communication needs `postMessage`, routing is a mess, and accessibility suffers. Use only when you genuinely need full sandboxing, like embedding third-party content. **Build-time integration (npm packages)** means MFs are published as packages and imported at build time. This loses independent deployment: updating one MF requires rebuilding and redeploying the host. Rarely what you actually want. ### When to use Use microfrontends when: - You have 3+ teams that need to ship without coordinating releases - Your app has clearly separated business domains (catalog, checkout, account) - You are migrating a legacy monolith and want to replace one section at a time - Teams genuinely disagree on tech stack and you can accept that complexity Skip microfrontends when: - The team is under 20 developers - You are building an MVP or early-stage product - The app is mostly static or content-heavy - A well-structured monorepo with clear module boundaries would give you 80% of the independence at 20% of the cost ### How the browser composes microfrontends The browser fetches the shell HTML. SystemJS (or native ES modules) handles dynamic imports via `System.import()`. Each MF bundle registers a lifecycle with the orchestrator. On route change, the orchestrator checks `window.location`, calls `mount()` on active MFs and `unmount()` on inactive ones. CSS isolation comes from Shadow DOM (`element.attachShadow({ mode: 'open' })`) or scoped CSS Modules. Cross-MF communication goes through Custom Events on `window` or BroadcastChannel for cross-origin scenarios. In practice, teams consistently underestimate the unmount lifecycle. Skip it and you accumulate event listener leaks on every navigation, which becomes very visible in long-running SPA sessions. ### Common mistakes **Global CSS leaks across MFs.** One team adds `.btn { color: red !important }` to their global stylesheet and every button in every other MF turns red. ```css /* Wrong: no scope, cascades everywhere */ .btn { color: red !important; } /* Breaks every sibling MF */ /* Fix: CSS Modules scopes it to the component */ /* MF1.module.css */ .btn { color: blue; } /* Only applies inside MF1 */ ``` Use CSS Modules or Shadow DOM. Shadow DOM is stricter but requires more setup. **React loaded five times.** Without Module Federation's singleton config, each MF bundles its own copy of React. Five MFs means roughly 5 x 130kb of duplicate bundle weight. ```javascript // webpack.config.js - required for shared dependencies new ModuleFederationPlugin({ shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' } } }) ``` **Tight coupling through `window` variables.** Teams use `window.userState = {}` as a shared store. Two MFs overwrite it on mount in a race condition. Use Custom Events or BroadcastChannel instead. ```javascript // Wrong: shared mutable global window.userState = { userId: 123 }; // Overwritten by next MF that mounts // Fix: event-based communication window.dispatchEvent(new CustomEvent('user-login', { detail: { userId: 123 } })); // For cross-origin MFs (iframe-based or separate origins): const channel = new BroadcastChannel('mf-channel'); channel.postMessage({ type: 'user-login', userId: 123 }); ``` **Direct DOM manipulation across MF boundaries.** MF A does `document.getElementById('mf-b-root').innerHTML = ''`. This breaks MF B's unmount lifecycle and causes framework errors about unmounted trees. Never touch another MF's DOM directly. **Skipping the unmount lifecycle.** If your MF does not clean up event listeners and timers on unmount, you accumulate leaks every time the user navigates. Always implement unmount in single-spa or return a cleanup function in Module Federation. ### Real-world usage - Netflix runs 100+ MFs via single-spa; the shops UI team deploys weekly without touching the recommendations engine - IKEA uses Module Federation with separate landing, tradfri, and catalog MFs in their own repos - Spotify uses Webpack Module Federation; podcast and player MFs share an RxJS singleton - Zalando runs 200+ MFs with the Luigi framework, mixing Angular and React across teams - Teams typically reach for microfrontends when cross-team deploys happen more than once a week and coordination costs become visible in sprint planning ### Follow-up questions **Q:** How do you handle shared dependencies like React across MFs? **A:** Version-align through Module Federation singletons: `shared: { react: { singleton: true, strictVersion: true } }`. If versions diverge, the bundle fallback loads a second copy, which is the signal to fix the mismatch. **Q:** What is the performance cost of runtime composition? **A:** Initial load adds roughly 200-500ms for extra JS round trips. Mitigate with `<link rel="modulepreload">` for predictable MF bundles and lazy-loading for routes the user has not visited yet. **Q:** How do you sync state across MFs without a global Redux store? **A:** BroadcastChannel handles loose events between MFs, including across iframes. URL params carry navigation state. For tighter sync (like a cart count badge in the header), a small shared state module exposed via Module Federation works, but keep it minimal. **Q:** Shadow DOM or CSS-in-JS for style isolation? **A:** Shadow DOM gives total isolation using constructable stylesheets (Chrome 89+). CSS-in-JS (Styled Components, Emotion) gives scoped styles with less strict boundaries. Shadow DOM is safer across framework boundaries; CSS-in-JS is easier if all MFs share one framework. **Q:** (Senior) In a hybrid migration from a monolith to microfrontends, how do you avoid downtime? **A:** Proxy the legacy route `/old` to the monolith via iframe or reverse proxy. Deploy the new MF behind a feature flag. Use single-spa import maps to swap the module URL at runtime: `"legacy-app"` points to the monolith bundle today and to the new MF bundle after cutover. Rolling back means updating the import map, no redeploy needed. **Q:** How does SSR work with microfrontends? **A:** The shell SSRs its static structure plus a hydration map. Each MF SSRs its own chunk, stitched together via Edge-Side Includes (ESI) at the CDN layer. Podium is a framework built for this pattern. Most teams start with CSR and add SSR per MF only where SEO or FCP metrics actually matter. ## Examples ### Module Federation: shell loads a Vue cart widget into a React product list ```javascript // shell/webpack.config.js const { ModuleFederationPlugin } = require('webpack').container; module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'shell', remotes: { // Cart MF lives at its own URL cart: 'cart@http://localhost:3002/remoteEntry.js', }, shared: { react: { singleton: true }, 'react-dom': { singleton: true } } }) ] }; ``` ```jsx // shell/src/ProductList.jsx import React, { lazy, Suspense } from 'react'; // Loaded from the remote Vue MF at runtime const Cart = lazy(() => import('cart/CartWidget')); export default function ProductList() { return ( <div> <h2>Products</h2> <Suspense fallback={<div>Loading cart...</div>}> <Cart /> {/* Vue component renders inside a React tree */} </Suspense> </div> ); } ``` When the user loads `/products`, the shell fetches `remoteEntry.js` from `localhost:3002`, gets the Cart bundle, and React renders it via Suspense. The Vue team deploys a new cart version and users see it on next page load. No shell redeploy needed. ### Cross-MF communication with BroadcastChannel Custom Events work for same-origin MFs. For cross-origin or iframe-based MFs, BroadcastChannel is the right tool. ```javascript // Navbar MF (React) - user logs in, broadcasts to all other MFs function Navbar() { const handleLogin = () => { const channel = new BroadcastChannel('mf-events'); channel.postMessage({ type: 'user-login', userId: 123 }); }; return <button onClick={handleLogin}>Login</button>; } // Dashboard MF (Vue) - receives the event // In mounted() or setup() const channel = new BroadcastChannel('mf-events'); channel.addEventListener('message', (event) => { if (event.data.type === 'user-login') { console.log('User logged in:', event.data.userId); // Update local state } }); // Clean up when MF unmounts - always channel.close(); ``` BroadcastChannel works across separate browsing contexts on the same origin. If you use iframes with different origins, you need `postMessage` with an explicit `targetOrigin`. ### Legacy migration: old Angular app running alongside a new React MF ```javascript // single-spa root-config.js import { registerApplication, start } from 'single-spa'; // Legacy monolith section (Angular, not yet replaced) registerApplication({ name: 'legacy-checkout', app: () => System.import('http://legacy.internal/checkout.js'), activeWhen: ['/checkout'], }); // New React MF - same route, gated by feature flag registerApplication({ name: 'new-checkout', app: () => System.import('http://mf-checkout.internal/remoteEntry.js'), activeWhen: ['/checkout'], customProps: { enabled: window.__FLAGS__?.newCheckout ?? false, }, }); start(); ``` The feature flag switches traffic between legacy and new. Once the new MF passes QA, flip the flag. Both apps coexist during migration. The Angular team keeps shipping; the React team builds alongside them with no shared deploy pipeline.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.