Learn the concept
Frontend Scalability Patterns
A micro-frontend architecture decomposes a monolithic frontend into smaller, independently deployable applications owned by autonomous teams, using integration techniques like Webpack Module Federation, Single-SPA, or Web Components, while sharing a design system and communicating through custom events or a shared state bus.
Micro-frontends extend the principles of microservices to the frontend, allowing multiple teams to build, test, and deploy their portions of a web application independently. This is not a default architectural choice — it introduces significant complexity and is best suited for large organizations with multiple autonomous teams working on distinct product domains.
Module Federation, introduced in Webpack 5, allows separately built applications to share modules at runtime. A host (shell) application declares which remotes it consumes, and each remote exposes specific modules. Shared dependencies like React are loaded once as singletons.
Pros: Native Webpack support, eager/lazy loading, granular sharing, versioned fallbacks. Cons: Tight coupling to Webpack (though Rspack and Vite now have compatible plugins).
A meta-framework that orchestrates multiple single-page applications. Each micro-frontend registers itself with lifecycle hooks (bootstrap, mount, unmount), and Single-SPA activates the correct one based on the URL.
Pros: Framework-agnostic, battle-tested, supports lazy loading. Cons: More boilerplate, shared dependency management is manual.
The simplest isolation model. Each micro-frontend renders in its own iframe with complete DOM and JavaScript isolation.
Pros: Perfect isolation, impossible to leak styles or global state. Cons: Poor UX (no shared scrolling, accessibility challenges), difficult deep linking, performance overhead from multiple browser contexts.
Each micro-frontend exposes custom elements. The shell app renders them as standard HTML tags.
Pros: Framework-agnostic, native browser support, Shadow DOM style isolation. Cons: Server-side rendering is complex, React's synthetic event system can conflict with Shadow DOM.
Duplicate bundles are the biggest performance concern. Use the singleton pattern to load React, React DOM, and your design system exactly once:
singleton: true and requiredVersion.externals and load from a CDN or the shell.<script type="importmap">) to point bare specifiers to shared CDN URLs.The shell application owns the top-level router. It maps URL prefixes to micro-frontends (e.g., /checkout/* loads the checkout remote). Within their prefix, each micro-frontend manages its own internal routing. Deep linking works because the shell delegates everything after the prefix.
Micro-frontends should be as decoupled as possible. When communication is necessary:
CustomEvent on window, and another listens for it. This keeps coupling to an event name contract.Visual consistency requires a shared component library published as an npm package or exposed via Module Federation. Key practices:
<link rel="preload"> for remote entries and critical chunks.// webpack.config.js — Remote (Checkout micro-frontend)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
output: {
publicPath: 'https://checkout.example.com/',
uniqueName: 'checkout',
},
plugins: [
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: {
'./CheckoutPage': './src/CheckoutPage',
'./CartWidget': './src/CartWidget',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
'@company/design-system': { singleton: true },
},
}),
],
};
// webpack.config.js — Host (Shell application)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
account: 'account@https://account.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, eager: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, eager: true, requiredVersion: '^19.0.0' },
'@company/design-system': { singleton: true, eager: true },
},
}),
],
};
// Shell app — lazy-loading a remote micro-frontend
const CheckoutPage = React.lazy(() => import('checkout/CheckoutPage'));
function App() {
return (
<React.Suspense fallback={<LoadingSkeleton />}>
<Routes>
<Route path="/checkout/*" element={<CheckoutPage />} />
</Routes>
</React.Suspense>
);
}Separate teams own checkout, product catalog, search, and user account flows — each deployed independently with their own release cycles and tech stacks
Multiple business units contribute analytics widgets, reporting tools, and admin panels to a shared shell without coordination bottlenecks
Migrating from Angular to React page by page, with both frameworks running simultaneously under a shell app, rather than a risky big-bang rewrite
Build a shell app with two remote micro-frontends (a todo list and a weather widget) using Webpack Module Federation, with shared React and a custom event bus for communication
Create a deployment manifest service that allows updating micro-frontend remote entry URLs without redeploying the shell, with rollback capability
Uses micro-frontends to allow dozens of teams to independently develop and deploy parts of their e-commerce experience
The Spotify desktop app uses iframe-based micro-frontends to compose independently developed features into a unified experience