Micro-Frontend Architecture: Splitting Large Applications
When a frontend outgrows a single repository, micro-frontends split it much like microservices. This article covers the motivations and costs, Module Federation, iframes, Web Components, and route-based distribution, their trade-offs, and why most teams should stay with a monorepo.
Key Points
- Topic Focus: React、TypeScript
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
1. What Are Micro-Frontends? Microservices for the Frontend
First, let us clear up a common misconception: micro-frontends are not a specific framework or a new technology. They are an architectural style.
In the backend world, when a Monolith grows to hundreds of thousands of lines of code, dozens of teams modify it simultaneously, and every release forces everyone to wait in line, teams split it into microservices. Each service can be deployed, scaled, and built with its own technology stack independently, and services communicate through HTTP/gRPC.
When the frontend reaches the same crossroads, the situation often looks like this: an operations dashboard, SaaS console, or e-commerce storefront has a React/Vue codebase tens of MB in size; a Webpack cold start takes five minutes; changing one button requires the entire CI process; and five business teams argue over the same release window. Someone then opens the microservices playbook and asks: can we split the frontend too, allowing each team to deploy its own part independently?
That is the micro-frontend approach. ThoughtWorks first added it to its Technology Radar in 2016, with a core definition that can be summarized in one sentence: split one frontend application into multiple smaller applications that can be independently developed, deployed, and delivered, then compose them at runtime into a unified experience for users.
The key phrase is independent deployment. Simply dividing code into directories or extracting components is only project organization. In a micro-frontend architecture, each sub-application has its own build pipeline, release cadence, and production environment, and they are ultimately assembled into a complete page in the user's browser.
That sounds attractive. We will discuss the cost later, but first let us examine how it can be implemented.
2. Four Mainstream Implementation Approaches
The technical approaches to implementing micro-frontends all answer one essential question: how does application A load application B and render it on the same page? There are four mainstream answers, each with its own trade-offs.
1. Module Federation
Module Federation is a feature introduced in Webpack 5, and the Rspack/Vite ecosystems have since adopted it. It allows one application to import modules from another application's build output at runtime, as naturally as importing local modules.
// host 应用的 webpack 配置
new ModuleFederationPlugin({
remotes: {
dashboard: 'dashboard@https://cdn.x.com/remoteEntry.js',
},
})
// host 应用里直接 import 远程组件
const Dashboard = React.lazy(() => import('dashboard/Dashboard'))
Remote modules are fetched at runtime without the local code needing to treat them differently. This is currently the mainstream approach. It does not rely on the hard isolation of iframes or the abrupt switching of route-based distribution. It offers a high level of runtime integration and a development experience closest to a monorepo.
The cost is deep coupling with the Webpack/Rspack/Vite build pipeline and a substantial configuration burden. TypeScript type sharing, version negotiation, and duplicate instances, such as React being loaded twice and immediately throwing an error, all require dedicated handling. The official Vite plugin vite-plugin-federation still uses Rollup for production builds, and asynchronous chunks have more edge cases than they do with Webpack, so a complete regression test is required before production deployment.
2. iframe
This is the oldest, simplest, and most frequently criticized approach: place the entire sub-application inside an iframe.
Its advantage is natural isolation. CSS, JavaScript, global variables, and dependency versions cannot interfere with one another, and different technology stacks, such as React + Vue + plain HTML, can coexist. Its drawbacks are just as pronounced:
- Poor performance: Each iframe creates an independent browser context, increasing memory usage and slowing the initial render.
- Awkward UX: Modals, dropdown menus, and toast notifications are clipped by iframe boundaries by default. Nested scrolling, focus traps, and a11y all require separate fixes.
- Cumbersome communication:
postMessageis the only channel, while cross-origin deployments also require handling cookies and propagating authentication state. - History, SEO, printing, copy and paste, keyboard shortcuts, and nearly every other browser feature require patches.
An iframe is appropriate for "embedding third-party content," such as an independent tool panel that must not affect the host page. It is not suitable as the primary product shell. Even so, many teams begin their micro-frontend experiments with iframes because they can ship the result in two days.
3. Web Components (Custom Elements)
A sub-application is exposed as a custom element such as <my-dashboard></my-dashboard>, and the host uses it like an ordinary HTML tag. Shadow DOM provides style isolation, and the approach is framework-agnostic because React, Vue, and Svelte can all output Web Components.
Its appeal is framework neutrality. A React host and a Vue sub-application work together without issue because both ultimately become DOM elements. Design systems are also often distributed as Web Components, enabling natural reuse across technology stacks.
The drawback is that you must build your own state management, routing, and SSR integration. Shadow DOM style isolation can become a burden in some design-system scenarios because theme switching, global fonts, and a11y focus management must cross the shadow boundary. Historically, using Web Components in React also involved many property-passing problems, such as className not working. React 19 largely resolved these issues. This approach is better suited to component-level integration than to integrating complete sub-applications.
4. Route-Based Distribution (Nginx/Reverse Proxy)
This is the simplest "pseudo-micro-frontend" approach: map different paths to different applications and use Nginx or a gateway to route requests to separate static asset servers. /dashboard uses application A, /orders uses application B, and users navigate between pages through links.
Strictly speaking, this is not "runtime composition" because switching applications triggers a full-page reload. Many teams still use it as an entry point to micro-frontends because it requires almost no migration work, is the easiest approach to deploy, and provides the strongest isolation.
The drawback is equally obvious: it is not a true single-page application. Sub-applications cannot share state or transition smoothly, and users see a loading state on every navigation. It works well when business lines are naturally isolated and users rarely switch between them, such as separate SaaS modules or operations tools.
Comparison Table
| Approach | Isolation | Integration | Performance | Complexity | Typical scenario |
|---|---|---|---|---|---|
| Module Federation | Medium (requires manual handling) | High | Medium | High | Multiple teams using the same stack; host application + multiple submodules |
| iframe | High | Low | Low | Low | Third-party content or extremely strict isolation requirements |
| Web Components | Medium-high | Medium | Medium | Medium | Design systems and components shared across technology stacks |
| Route-based distribution | High | Extremely low | Medium, but with full-page reloads | Extremely low | Naturally isolated business lines with infrequent switching |
There is no "best" approach, only the approach that best fits the current constraints. The central selection question is whether your sub-applications must "form one continuous experience" or remain "independent tools." Only Module Federation truly fits the former. Route-based distribution or iframes are sufficient for the latter.
3. Shared Concerns: Design Systems, Dependencies, and State
Once the application has been split, sharing immediately becomes a problem. If a company has six sub-applications, each with its own design system, React version, and authentication state, that is not a micro-frontend architecture. It is six teams independently reinventing the same wheel.
Sharing the design system is the easiest task and the one that should be addressed first. Create one unified set of design tokens for colors, spacing, font sizes, border radii, and motion curves, along with a foundational component library containing Button, Input, Modal, and Table. Publish them through an internal private npm registry and install them as dependencies in every sub-application. Design tokens can be injected at runtime through CSS variables to support theme switching. The core principle is that cross-application UI consistency comes from a "shared source design system," not from "synchronizing styles at runtime."
Dependency sharing is a strength of Module Federation. Libraries shared by multiple sub-applications, such as React, ReactDOM, and Lodash, can be configured as "shared" so they are loaded only once at runtime. Version negotiation requires attention: if sub-application A requires React 18.0 and sub-application B requires React 18.3, Federation negotiates a shared version according to semantic versioning, and every sub-application must accept it. In practice, teams should define a "shared dependency version range" and enforce it through CI checks.
State sharing is more subtle. Sharing a single store, such as Redux, couples sub-applications so tightly that they lose their independence. The recommended approach is for cross-application state to travel through "events + a communication layer", such as custom events, a lightweight EventBus, or postMessage, instead of sharing an in-memory store. Each sub-application uses its own internal store and communicates across application boundaries only through contract-based events. This is the same principle backend microservices use when communicating through events/Kafka.
Global context such as authentication state and user information is generally owned by the host application, or Shell, and injected into sub-applications through props or context. A sub-application does not know how authentication works; it only consumes a user object.
4. The Cost of Micro-Frontends
By this point, it should be clear that micro-frontends are not a free lunch. Every benefit carries a corresponding cost.
Complexity explodes. One build pipeline becomes N pipelines. Every sub-application needs its own CI/CD, environment variables, version number, and rollback mechanism. If the version of a Module Federation remote entry (remoteEntry.js) drifts, the host may load an incompatible sub-application. You must either introduce contract testing or accept occasional production incidents. Deployment order also becomes a problem: should the host or the sub-application be upgraded first?
Performance problems. Even with shared dependencies, code splitting across sub-applications is coarser than in a monorepo. The initial render often requires fetching multiple remoteEntry files, making the loading chain longer. Without SSF(Server-Side Fetch) coordination, a long waterfall remains after TTFB. The memory overhead of the iframe approach is especially severe; opening five tabs can bring the browser to its knees.
Risk to UI consistency. Although the design system is shared, every team introduces its own "small innovation": one adjusts the spacing, while another creates a custom button. Without governance, the six sub-applications will look like products from six different companies after six months. A dedicated design-system team (SDK Team) must enforce standards and conduct regular reviews.
Communication cost. Cross-application communication requires designed contracts, versions, and backward compatibility. If sub-application A emits an event and sub-application B fails to listen for it, debugging crosses repositories and teams, causing the investigation cost to rise exponentially.
Organizational cost. Micro-frontends require teams to be divided along business boundaries, with a clear owner for each sub-application. If your organization is divided by function, with separate frontend, backend, and design groups, forcing a micro-frontend architecture will only create additional cross-team coordination costs. Conway's law applies once again: architecture shapes the organization, and the organization imposes requirements on the architecture.
5. When to Use Micro-Frontends and When Not To
Given these costs, when are micro-frontends actually worthwhile? Here are three strict criteria. Seriously consider the architecture only if you meet at least two:
- A very large team with multiple business lines. The frontend organization has more than 50 people, or at least three independent business teams must iterate in parallel and the business boundaries are clear. A ten-person team has absolutely no need for micro-frontends.
- A strong need for independent deployment. Different business lines have entirely different release cadences, compliance requirements, or canary strategies and block one another inside a single repository. For example, a financial product may require strict auditing for every release, while an operations product must ship small updates several times per day. Combining them causes each to slow the other down.
- Heterogeneous technology stacks. Some parts of the business must use different technology stacks because of legacy constraints or specialized requirements, such as Vue + React or an old Angular application alongside a new React application, and a unified migration is not feasible.
Conversely, do not use micro-frontends in these situations:
- Small or medium-sized teams, ranging from a few to a dozen frontend engineers: A single repository + Monorepo is entirely sufficient, and micro-frontend complexity far exceeds the benefits.
- One technology stack and one business line: There is no team-collaboration bottleneck or need for independent deployment, so micro-frontends deliver negative value.
- To "look advanced": The technology choice is driven by resumes rather than business needs.
- To "solve slow builds": That is an engineering problem. Switching to Vite/Turbopack, adding build caching, or improving bundle splitting can solve it without introducing an architectural-level micro-frontend system.
- To "split now so expansion will be easier later": This is premature optimization. By the time splitting is genuinely necessary, the shape of the business will already have changed.
6. Most of the Time, Monorepo + a Component Library Is Enough
I have seen too many teams treat micro-frontends as a "silver bullet for frontend project organization," only to discover six months later that they are maintaining something three times more complicated than the original monolith and quietly roll it back. In most scenarios, Monorepo + a design system + a component library is a much better solution than micro-frontends.
A Monorepo using pnpm workspace, Nx, or Turborepo gives you:
- Naturally simple code sharing: Import design systems, utility functions, and type definitions directly from packages in the same repository.
- Atomic cross-package changes: Refactor a component used by more than a dozen applications and update everything in one PR. With micro-frontends, the same change requires coordinating releases across N repositories.
- A unified build toolchain: One CI system, one lint configuration, and one tsconfig make onboarding faster.
- Incremental builds: Turborepo/Nx build caching allows CI to run only the affected parts, making it more efficient than maintaining separate CI for every sub-application.
A Monorepo solves a code organization problem, while micro-frontends solve deployment and organizational boundary problems. The former is a subset of the latter. There is a reason to move to micro-frontends only when code organization can no longer satisfy the need for "independent deployment" and "alignment with team boundaries."
My recommendation is: start with a Monorepo + an internal component library + a design system, and establish strong engineering governance. If one day a package truly needs an independent release cadence, a team must use a different technology stack, or a business unit must scale independently, extract that package into a standalone sub-application and migrate it to a micro-frontend architecture. Micro-frontends are the result of evolution, not the starting point.
With a well-built component library and design system, even a future split will be relatively smooth because the sub-application boundaries are already encoded in the package structure. Conversely, starting with micro-frontends and later trying to merge everything back into one repository is nearly a nightmare.
Summary
- Micro-frontends = microservices for the frontend: The core principles are independent development, independent deployment, and runtime composition, not a specific framework or technology.
- The four implementations serve different purposes: Module Federation is the mainstream approach for runtime integration; iframes provide strong isolation but poor UX; Web Components suit cross-stack components; and route-based distribution is simplest but is not an SPA.
- A shared design system is the baseline: Distribute design tokens + a component library through a private npm registry, and achieve consistency across sub-applications through a common source rather than synchronization.
- The costs are real: Build and deployment complexity, performance overhead, UI governance, and cross-team collaboration must all be accounted for.
- The threshold is higher than it appears: Seriously consider micro-frontends only when at least two conditions apply, such as more than 50 frontend engineers, independent deployment across multiple business lines, and heterogeneous technology stacks.
One final takeaway for anyone making this decision: Monorepo + a component library is the best solution for 80% of teams. Micro-frontends are a last resort, not the default. When you are unsure whether to adopt micro-frontends, the answer is almost always "do not."
Related
Related Content
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
The Engineering Value of TypeScript in AI Application Frontends
Technical trends, engineering practices, and an independent developer perspective on TypeScript and React.
Article
What Value Do Server Components Offer Content Sites?
Technical trends and engineering practices around Next.js and React, from an independent developer's perspective.