Splitting a frontend sounds great in architecture diagrams. The reality is messier.
Micro-frontends have been around since 2016. For most of that decade, they were either overhyped conference talks or nightmarish production systems held together by iframes and prayer. But something shifted around 2024-2025. The tooling got good enough. The patterns matured. And the industry collectively figured out that micro-frontends are not a default architecture -- they are a solution to a specific organizational problem.
We have deployed micro-frontend architectures for three clients at CODERCOPS over the past two years. We also ripped one out entirely for a fourth client and replaced it with a monolith. Both decisions were correct. That tension -- knowing when to use them and when to walk away -- is what this post is really about.
What Micro-Frontends Actually Are
Let me be blunt: micro-frontends are not about technology. They are about team boundaries.
A micro-frontend is an independently deployable piece of a web application, owned by a single team, that composes with other pieces to form a complete user experience. That is it. The technical implementation -- whether you use Module Federation, iframes, web components, or server-side includes -- is secondary to the organizational question: do you have multiple teams that need to ship frontend changes independently without coordinating releases?
If the answer is no, you almost certainly do not need micro-frontends. A well-structured monolith with clear module boundaries will serve you better, cost less, and ship faster. I cannot stress this enough. We have seen startups with 8 developers split across 3 micro-frontends because they read a ThoughtWorks blog post. Don't be that team.
But if you have 4+ teams, each owning a distinct domain of a large application -- checkout, product catalog, user accounts, admin dashboard -- and deployments are bottlenecked by cross-team coordination, then micro-frontends solve a real problem. The question becomes how to implement them.
The Four Composition Patterns
There are four primary ways to compose micro-frontends into a single application. Each makes a different set of tradeoffs between performance, complexity, and team autonomy. We have used all four in production at various points.
Build-Time Composition
This is the simplest approach. Each micro-frontend is published as an npm package, and a shell application imports them as dependencies during the build step. Think of it like a monorepo where each package is owned by a different team.
The problem is obvious: you still need a coordinated build and deploy. Team A publishes @company/checkout-mfe@2.3.1, and someone has to update the shell app's package.json and trigger a build. That "someone" becomes a bottleneck. You have traded runtime complexity for release coordination overhead, and in our experience, the coordination overhead is worse. It defeats the entire point of independent deployability.
We used build-time composition for exactly one project. It worked because the client had only two teams and released on a weekly cadence anyway. For anything more dynamic, avoid it.
Server-Side Composition
The server assembles a complete HTML page from fragments produced by different micro-frontends. This can happen at the origin server (using something like Podium, Tailor, or even Nginx SSI directives) or at the edge (Cloudflare Workers, Vercel Edge Middleware).
Server-side composition gives you excellent performance -- the user receives a fully rendered page with no client-side assembly overhead. But it is harder to build rich, interactive transitions between micro-frontends. Each fragment is essentially its own island of HTML, and coordinating client-side state across fragments requires extra plumbing.
We like this pattern for content-heavy applications where performance is non-negotiable. Think e-commerce product listing pages, news sites, or internal portals where time-to-first-byte matters more than single-page-app-style transitions.
Client-Side Composition
The shell application loads micro-frontends at runtime in the browser, typically via JavaScript. This is what single-spa pioneered, and it is what Module Federation enables at the bundler level. The shell manages routing and lifecycle, and each micro-frontend mounts into a designated DOM node.
Client-side composition gives you the most flexibility. Teams can use different frameworks (React team A, Vue team B), deploy independently, and share state through well-defined contracts. But the performance cost is real. The browser has to download, parse, and execute multiple JavaScript bundles before the page is interactive. On a fast connection with a modern device, this is fine. On a 3G connection in rural India -- which is a real concern for several of our clients -- the experience degrades noticeably.
Edge-Side Composition
This is the newest pattern and the one I am most excited about. Edge functions (Cloudflare Workers, Deno Deploy, Vercel Edge) assemble the page at the CDN edge, close to the user. Each micro-frontend is a standalone edge function that returns an HTML fragment, and the edge assembles them into a complete page.
The latency benefits are significant -- you get server-side composition performance with geographic distribution built in. But the tooling is still young. Debugging edge-composed applications is painful, observability is lacking, and you are tied to a specific edge platform's runtime constraints. We have experimented with this pattern on internal projects but have not shipped it for a client yet. The jury is still out on whether the developer experience catches up to the architectural promise.
Comparison Table
| Pattern | Independent Deploy | Performance | Framework Agnostic | Complexity | Best For |
|---|---|---|---|---|---|
| Build-time | No (coordinated builds) | Excellent | No | Low | Small teams, infrequent releases |
| Server-side | Yes | Very Good | Yes | Medium | Content-heavy, performance-critical |
| Client-side | Yes | Moderate | Yes | High | Complex SPAs, many teams |
| Edge-side | Yes | Excellent | Yes | High | Geo-distributed, latency-sensitive |
Module Federation 2.0 -- What Actually Changed
Module Federation was Webpack 5's flagship feature back in 2020, and it was genuinely innovative. The idea was simple: let one Webpack build consume modules from another Webpack build at runtime, without npm packages or build-time coordination. In practice, the original implementation was brittle. Version conflicts, shared dependency nightmares, and cryptic runtime errors made it frustrating to work with.
Module Federation 2.0 (which shipped as part of the @module-federation/enhanced package and works with both Webpack and Rspack) fixes the biggest pain points. Here is what matters:
Type safety across remotes. The @module-federation/typescript plugin generates .d.ts files from remote modules and makes them available to the host at development time. This was a massive gap in v1. You were essentially importing any from a URL and hoping for the best. Now you get full IntelliSense and compile-time type checking across micro-frontend boundaries.
Runtime plugins. MF 2.0 has a plugin system that hooks into the module loading lifecycle -- beforeInit, init, beforeLoadShare, loadShare, and so on. This means you can implement custom versioning strategies, fallback logic, and A/B testing without hacking the internals.
Framework-agnostic bridges. The @module-federation/bridge-react and @module-federation/bridge-vue3 packages provide standardized ways to mount remote components with proper lifecycle management. No more manual ReactDOM.render calls and cleanup hacks.
Manifest protocol. Instead of hardcoding remoteEntry.js URLs, MF 2.0 uses a manifest file that describes the remote's available modules, their types, and version requirements. This is huge for production operations -- you can update remotes without touching the host configuration.
Here is a minimal example of a Module Federation 2.0 setup with type safety:
// Host application -- webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.example.com/mf-manifest.json',
catalog: 'catalog@https://catalog.example.com/mf-manifest.json',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
}),
],
};
// Host application -- using a remote component
// Types are auto-generated from the remote's manifest
import CheckoutCart from 'checkout/Cart';
import ProductGrid from 'catalog/ProductGrid';
function App() {
return (
<div>
<ProductGrid
category="electronics"
onAddToCart={(item) => cartStore.add(item)}
/>
<CheckoutCart />
</div>
);
}// Remote application (checkout) -- webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'checkout',
exposes: {
'./Cart': './src/components/Cart',
'./MiniCart': './src/components/MiniCart',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
}),
],
};The key difference from v1: notice we are pointing to mf-manifest.json instead of remoteEntry.js. The manifest is a structured contract between host and remote, not just a JavaScript entry point. And the type generation means your IDE knows exactly what props CheckoutCart accepts, even though it lives in a completely separate repository and deployment pipeline.
Our Experience -- Wins and One Painful Loss
The Win: Enterprise SaaS Platform
Last year, a fintech client came to us with a classic micro-frontend problem. Their platform had five product areas -- payments, invoicing, reconciliation, reporting, and settings -- each owned by a different team across three time zones (Bangalore, London, San Francisco). Deployments were a weekly ritual that required all five teams to coordinate a release window on Thursday evenings IST. When one team's changes broke another team's integration tests, the whole release would slip. They were shipping roughly once every two weeks instead of the weekly target.
We implemented client-side composition using Module Federation 2.0 with a thin React shell. Each product area became an independent micro-frontend with its own repository, CI/CD pipeline, and deployment. Shared state (authenticated user, permissions, theme) flows through a lightweight event bus backed by a React context in the shell.
The result: teams now deploy independently, multiple times per day. The payments team ships three times as often as they did before. And we have not had a cross-team deployment conflict in seven months.
But -- and this is important -- the initial setup took almost three months. The first month was just defining the contracts between micro-frontends: shared types, event schemas, routing conventions, CSS isolation strategy. If you think you can "just split the app" and figure out the boundaries later, you are in for a rough time.
The Win: Internal Tools Dashboard
A logistics company needed a dashboard that aggregated data from six internal tools. Each tool had its own backend team and frontend, built with different technologies (two React apps, one Vue app, one Angular app, and two server-rendered pages). Instead of rewriting everything, we used single-spa to compose them into a unified shell with shared navigation and authentication.
This is one of the few cases where the "different frameworks" argument for micro-frontends is legitimate. We did not choose framework diversity -- we inherited it. And single-spa handled it well enough. The user experience is not as smooth as a single-framework app (transitions between Vue and React sections have a perceptible flash), but it is dramatically better than the six separate browser tabs the users were switching between.
The Loss: E-Commerce Storefront
We learned the hard way that micro-frontends are a terrible fit for e-commerce storefronts where performance is the primary business metric.
A retail client wanted to split their storefront into four micro-frontends: header/navigation, product listing, product detail, and cart/checkout. On paper, this made sense -- each area had different update frequencies and was maintained by a different team.
In practice, the performance overhead was devastating. The initial page load went from 1.8 seconds to 3.4 seconds because the browser had to load four separate JavaScript bundles, resolve shared dependencies, and mount four independent React trees. On mobile, it was worse. LCP regressed by 40%, and the client's conversion rate dropped measurably during the A/B test.
We spent two months trying to optimize it -- lazy loading, preloading hints, shared chunk extraction, service worker precaching. We got the load time down to 2.6 seconds, but it was still slower than the monolith, and the code complexity had tripled. After an honest conversation with the client, we ripped out the micro-frontend architecture and went back to a well-structured monolith with clear module boundaries and a shared component library. The teams adjusted their release process instead of the architecture.
The lesson: if your primary concern is end-user performance and your teams can tolerate some deployment coordination, a monolith is faster. Period. Micro-frontends add network requests, duplicate framework code, and coordination overhead that you cannot fully optimize away.
When to Use Them, When to Avoid Them
After building (and unbuilding) these systems, here is our honest decision framework:
Use micro-frontends when:
- You have 4+ teams that need to deploy frontend changes independently
- Deployment coordination is a measurable bottleneck (not a hypothetical one)
- The application is large enough that a single build takes more than 5 minutes
- Teams own distinct domains with clear boundaries (not shared screens)
- You can afford 2-3 months of upfront architecture work before feature velocity returns
Avoid micro-frontends when:
- You have fewer than 3 frontend teams
- Performance (LCP, TTI) is your primary competitive advantage
- Your application has heavily interconnected UI -- lots of shared state, drag-and-drop across domains, real-time collaborative features
- You are a startup trying to move fast. Seriously. Build a monolith, ship features, and revisit architecture when team coordination actually becomes a bottleneck
- You want micro-frontends because they sound cool on your resume. I am not judging, but your users will
I'm still not sold on the "use different frameworks" argument that micro-frontend advocates love to make. Yes, MF 2.0 and single-spa technically allow a React app and a Vue app to coexist. But in practice, you are shipping two framework runtimes to the browser, your developers need to context-switch between frameworks, and shared component libraries become a nightmare. We only recommend framework diversity when you are integrating legacy systems that cannot be rewritten (like the logistics dashboard above). For greenfield projects, pick one framework and stick with it.
The Tooling in 2026
Here is where the major tools stand right now:
Module Federation 2.0 is the strongest option for client-side composition if you are already in the Webpack/Rspack ecosystem. The type safety and manifest protocol solve the biggest operational headaches of v1. It is production-ready and has significant adoption at scale (ByteDance built it, and they run it across dozens of products). The downside is that it is tied to Webpack-compatible bundlers. If you are on Vite, you are out of luck -- for now.
single-spa is the veteran. It is framework-agnostic, well-documented, and has a massive community. But it shows its age. The programming model is imperative (register apps, set up lifecycle hooks), and the developer experience lags behind MF 2.0. We still use it for brownfield projects where we need to integrate legacy applications, but for new builds, we prefer Module Federation.
Native Federation (by Manfred Steyer) is interesting because it is bundler-agnostic. It uses ES modules and import maps, which means it works with Vite, Webpack, Rollup, or anything else. The tradeoff is that it relies on browser-native module loading, which is slower than Webpack's optimized module system. Good for Angular projects (it has first-class Angular support), less proven for React/Vue.
Astro Islands are not technically micro-frontends, but they solve a related problem -- composing independently hydrated components on a single page. If your "micro-frontends" are really just interactive widgets on a mostly-static page (dashboards, embedded tools, marketing pages with dynamic sections), Astro's island architecture is simpler and faster. We have used it for internal tools where the overhead of full micro-frontends was not justified.
Qwik deserves a mention for its resumability model. Instead of hydrating the entire micro-frontend on page load, Qwik serializes the application state into HTML and only loads JavaScript when the user interacts with a specific component. This dramatically reduces the performance cost of composing multiple frontends. We have not used Qwik for a client project yet, but our internal prototypes suggest it could change the performance equation for client-side composition. One to watch.
| Tool | Bundler | Framework Support | Type Safety | Maturity | Our Verdict |
|---|---|---|---|---|---|
| Module Federation 2.0 | Webpack, Rspack | React, Vue, Angular | Yes (plugin) | Production-ready | Default choice for new projects |
| single-spa | Any | Any | Manual | Battle-tested | Best for brownfield/legacy |
| Native Federation | Any (ES modules) | Angular (best), React, Vue | Partial | Growing | Angular-first teams |
| Astro Islands | Vite | React, Vue, Svelte, Solid | N/A | Stable | Content sites with interactive sections |
| Qwik | Vite | Qwik | N/A | Maturing | Performance-critical composition |
Practical Advice
If you have read this far, you are probably evaluating micro-frontends for a real project. Here is what we would tell you if you were sitting across the table from us:
Start with the org chart, not the architecture diagram. Draw your team boundaries first. If one team would own three of the four proposed micro-frontends, you do not have a micro-frontend problem -- you have a monolith with one module that should be extracted. And that is fine.
Define contracts before writing code. Spend the first two weeks defining shared types, event schemas, routing conventions, and CSS scoping rules. Write them down. Put them in a shared package. This boring upfront work saves months of debugging later.
Measure the performance budget. Before splitting anything, establish performance baselines for your critical user journeys. Then set a hard performance budget: "the micro-frontend version must not regress LCP by more than 200ms." If you cannot meet the budget after optimization, the architecture is wrong for your use case. That is useful information, not a failure.
Use Module Federation 2.0 for greenfield, single-spa for brownfield. This is our default recommendation. Module Federation's developer experience and type safety are better for new builds. single-spa's flexibility with legacy code is better for integration projects.
Do not let the architecture outlive the problem. If your company reorganizes and those four independent teams become two teams, revisit whether you still need micro-frontends. Architectural decisions should reflect current organizational reality, not the org chart from two years ago.
And finally -- if you are a team of 5-15 developers building a product, just build a monolith with good module boundaries. Use a monorepo. Set up clear code ownership with CODEOWNERS files. Deploy from a single pipeline. You can always extract micro-frontends later if team coordination actually becomes the bottleneck. But most teams never get there, and that is perfectly fine.
The best architecture is the one that ships features to users without making your developers miserable. Sometimes that is micro-frontends. Usually, it is not.
Comments