Micro-frontend integration patterns: Module Federation vs import maps vs build-time
A practical comparison of the ways to compose a front-end from independently owned pieces (npm packages, server-side composition, Module Federation, native import maps, web components), and how to pick one.
"Micro-frontend" is not one technique. It is a goal, letting teams ship parts of a UI independently, and it has at least five distinct implementations, each making a different trade between isolation, simplicity and runtime risk. Picking the wrong one is expensive, because the integration seam is the hardest thing to change later.
This is a tour of the options along a single axis: when does composition happen, at build time, on the server, or in the browser.
Build-time integration (npm packages)#
Each team publishes its feature as a versioned package; the shell app installs them and bundles everything together.
// shell/package.json
{
"dependencies": {
"@acme/checkout": "4.2.0",
"@acme/search": "2.9.1"
}
}| Pros | Cons |
|---|---|
| One bundle, one React, no runtime negotiation | Every feature release needs a shell bump + redeploy |
| Full type safety across the seam | Teams are coupled to the shell release train |
| Trivial to reason about, debug, test | A slow shell pipeline slows everyone |
| Best performance (shared tree-shaking) | Not really independent deployment |
Server-side / edge composition#
Each micro-frontend renders its own HTML fragment; a composition layer stitches fragments into one document. Server-Side Includes, ESI at the CDN, Tailor/Podium-style layout services, or modern takes like fragment embedding in Astro.
<!-- layout service output -->
<body>
<!--#include virtual="/fragments/header" -->
<main>
<!--#include virtual="/fragments/product?id=42" -->
</main>
</body>| Pros | Cons |
|---|---|
| Great first paint; SEO-friendly HTML | Needs a server / edge composition tier to run and own |
| Fragments can use different stacks | Client-side interactivity across fragments is awkward |
| Failure of one fragment can be isolated to a slot | Shared client state / routing needs extra plumbing |
Client-side: Module Federation#
The shell loads remote modules over the network at runtime and shares libraries through a negotiated scope. Covered in depth in the Module Federation post; the short version:
| Pros | Cons |
|---|---|
| True independent deploy; hot-swap a remote with no shell redeploy | New runtime failure modes (remote down, singleton clash) |
| Rich shared-dependency semver negotiation | Version skew becomes a distributed coordination problem |
| Lazy by default; remotes cost nothing until rendered | Types, debugging and SSR all need extra infrastructure |
| Framework-agnostic, mature tooling (Webpack, Rspack) | Needs a platform team to own the shared contract |
Client-side: native import maps#
An import map tells the browser where bare specifiers resolve. It is a web standard, supported in all current evergreen browsers, and needs no bundler runtime at all.
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
"@acme/checkout": "https://cdn.acme.com/checkout/v4/index.js",
"@acme/search": "https://cdn.acme.com/search/v2/index.js"
}
}
</script>
<script type="module">
const { mountCheckout } = await import("@acme/checkout")
mountCheckout(document.getElementById("checkout-slot"))
</script>Every remote resolves import "react" to the same URL, so the browser fetches and evaluates React once and every micro-frontend shares that module instance. That is the entire "shared dependency" mechanism. No share scope, no negotiation, just URL identity.
What you get
- Zero runtime library; the browser does the resolution.
- Independent deploys: change a CDN URL in the map, or version the path (
/checkout/v4/). - Trivial mental model; native devtools; real source maps from the origin.
- Works well with unbundled ESM (
esm.sh,jsr) and with per-feature build outputs.
What you give up
- No semver negotiation. You pin exact URLs. Two features needing incompatible React majors cannot both use the shared entry. You would knowingly load two, which breaks context and hooks identity.
- No built-in fallback if a URL 404s or the CDN is down. You add that yourself.
- The map is usually static. Dynamic import maps and multiple maps are landing but support is uneven; generating the map at deploy time from a manifest is the pragmatic move.
- Bundling story. Shipping hundreds of unbundled ESM modules over HTTP/2 is fine at medium scale; very large apps still want per-feature bundles (which import maps happily point at).
- Older build pipelines need
es-module-shimsfor full coverage.
Client-side: web components as the seam#
Each micro-frontend ships a custom element (<checkout-app>). The shell just places tags. Often combined with import maps or single-spa for loading.
| Pros | Cons |
|---|---|
| Framework-agnostic boundary; strong DOM/style isolation via shadow DOM | Passing rich data / callbacks through attributes & events is clunky |
| Lifecycle is the platform’s (connectedCallback) | Shared context (theme, auth, router) needs a deliberate channel |
| Easy to embed the same widget in multiple hosts | SSR + hydration for custom elements is still rough |
The decision matrix#
| If this is true | Use |
|---|---|
| Teams already release together; you want ownership boundaries | Build-time (monorepo + packages) |
| First paint / SEO is critical and you can run an edge tier | Server-side composition |
| Independent deploy cadence + you can staff a platform team | Module Federation |
| Independent deploy cadence + platform libs move in lockstep | Import maps |
| Multiple host apps embed the same widget across stacks | Web components |
| You are not sure yet | Build-time now; keep features split so you can switch later |
BUILD TIME SERVER / EDGE BROWSER (runtime)
────────── ──────────── ────────────────
npm i @acme/* layout service import map / remoteEntry
│ │ include slots │ resolve specifiers
▼ ▼ ▼
one bundle one HTML doc shell + N async modules
one deploy N fragment deploys N independent deploys
no runtime risk tier to operate runtime negotiation / fallbackCross-cutting concerns you need regardless#
- Design system: one shared, versioned source of tokens/components, rolled forward on a schedule.
- Auth & session: one owner, exposed to features through a defined API, never re-implemented per feature.
- Routing: decide who owns the URL and how features claim sub-paths.
- Cross-feature events: a documented event bus or shared store, with a schema.
- Observability: every error and RUM sample tagged with which feature + version produced it.
- Failure UX: a standard skeleton/error boundary for "a feature failed to load".