
Why Enterprises Choose Module Federation
Néstor LópezThe Standards-Based Promise
For several years now, a recurring argument surfaces in micro-frontend discussions: if browsers support native ES Modules, import maps have reached baseline status, and frameworks can compile to web components, why maintain dependency on build-specific tooling like Module Federation?
The standards-only position runs something like this:
"Why use Module Federation's build-specific tooling when you can compose micro-frontends at runtime using self-contained web components? Why not just use native ESM with semantic URLs, pulling shared dependencies from CDNs like unpkg? Import maps are a standard now - isn't that enough?"
This argument makes sense. Browser support for ESM is universal. Import maps shipped across major browsers throughout 2023. Frameworks like Angular and Vue can generate standalone web components. The appeal is clear: no bundler lock-in, no proprietary abstractions, just the platform.
But there's a big gap between what works in demos and what survives production at scale.
Before diving into the technical details, let's acknowledge something important: we've been here before.
We've Seen This Pattern Before
Ten years ago, we solved micro-frontends by loading React on window with UMD bundles. It worked in demos but broke down in production.
ESM with import maps is essentially the same pattern with modern syntax:
- Global module scope (window versus import map)
- Manual version coordination
- Limited sharing logic
- Path-based resolution
It's similar to Webpack v1 externals—marking dependencies as external and loading them separately. That worked for simple cases but didn't scale.
The Real Problem:
The problem was never about loading scripts—that's the easy part. The hard part is managing them at scale:
- What happens when you have 500 remotes with 70 shared dependencies each?
- How do you automatically handle version conflicts?
- How do you manage memory in applications that stay open for days?
- How do you tree-shake and optimize shared dependencies across apps?
- How do you handle errors and provide fallbacks?
Module Federation solves the management problem, not just the loading problem. Import maps only solve loading.
The Reality Check
The standards-only pitch sounds compelling: no bundler lock-in, no proprietary abstractions, just browser standards. Use native ESM with semantic URLs, pull shared dependencies from CDNs, and compose everything at runtime.
But there's a gap between what works in demos and what survives production at scale. Let's start with the biggest challenge—the one that affects developers every single day.
The Development Experience Problem
This is the main issue we're solving with Zephyr.
When your application spans multiple CDN origins, debugging becomes painful:
- Source maps scattered across multiple domains
- DevTools showing files from everywhere, making navigation difficult
- Stack traces that jump between origins
- Performance profiling fragmented across domains
- Network waterfalls cluttered with cross-origin requests
Module Federation keeps everything within your application's context:
- Unified source maps from your bundler
- Stack traces that make sense
- Integrated performance monitoring
- Single domain in DevTools
- Clear debugging flow
The real cost: Hours wasted debugging issues that would be straightforward in a unified environment. Developer productivity takes a massive hit.
Now let's look at the other technical challenges that compound this problem.
Other Enterprise Reality Gaps
1. Infrastructure and Security Constraints
The CDN Problem:
Public CDNs like unpkg or esm.sh work great for prototypes and small projects. But most enterprises can't rely on external infrastructure in production due to:
- Internal package registries behind corporate firewalls
- Strict dependency audit requirements and vulnerability scanning
- Air-gapped deployments in regulated industries (finance, healthcare, defense)
- Data sovereignty requirements that prohibit external service dependencies
- Service-level agreements that demand control over availability and latency
Companies like Microsoft, ByteDance, and Amazon don't architect production systems around external CDNs they can't control. This isn't paranoia; it's operational pragmatism.
Consider the dependency chain:
// This appears straightforward...
import React from 'https://esm.sh/react@18.2.0';
// But production environments must address:
// - What happens when esm.sh experiences an outage?
// - How do we perform SBOM-based vulnerability audits?
// - How do we meet internal SLA requirements (e.g., 99.99% uptime)?
// - How do we serve users in regions with poor connectivity to esm.sh?
// - Where do internal/proprietary packages live that must never reach public CDNs?
Depending on external URLs for core functionality creates single points of failure. You face inconsistent latency for global users, security risks from third-party infrastructure, and loss of control over versioning—a CDN operator's change can break your production app.
The URI Problem:
URIs assume stable, uniform addressing across deployment contexts. Real-world applications across teams, business units, or partner companies rarely share identical infrastructure, domains, or URL schemes. The assumption that a stable URL like https://unpkg.com/react@18 can serve as a global singleton identifier breaks down when apps deploy to different domains, regions, and environments with varying network topologies.
2. CORS and Cross-Origin Complexity
Native ESM operates within browser security boundaries; Module Federation does not.
When import maps reference external URLs, every module load must satisfy Cross-Origin Resource Sharing policies. This creates problems:
// Import map approach
{
"imports": {
"react": "https://cdn.example.com/react@18/index.js",
"react-dom": "https://cdn.example.com/react-dom@18/index.js"
}
}
Each referenced domain requires proper CORS configuration. OPTIONS preflight requests add latency to cross-origin fetches. Debugging CORS failures across multiple domains becomes increasingly difficult as the number of origins grows. Cross-domain authentication introduces additional complexity around credentials and security policies.
Module Federation operates within your application's bundled scope, bypassing these cross-origin restrictions. Remote modules load programmatically at runtime through your application's own domain context, avoiding the browser's cross-origin security model entirely for federated code.
This isn't a workaround; it's a fundamental architectural difference in how code sharing works at the bundler level versus the browser level.
3. Import map limitations and fragility
Import maps work for certain use cases, but they have constraints that show up in dynamic, complex architectures.
While import maps have reached baseline browser support, they have limitations in production:
No Fallback Recovery:
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.example.com/lodash@4.17.21/index.js"
}
}
</script>
<script type="module">
// If the import map fails to load or parse, module resolution fails.
// You can catch dynamic import errors, but not static import map parsing failures.
import _ from 'lodash';
</script>
If the import map itself fails to fetch or parse, static imports mapped by it won't resolve. Mitigation strategies exist—inlining a minimal import map in HTML, using a loader script with retry logic, serving maps from your own origin with aggressive caching—but these add operational complexity.
Browser and Runtime Gaps:
Import maps have broad browser support, but gaps remain on older mobile browsers and embedded webviews. Node.js supports import maps via flags that vary by version, so SSR support exists but isn't seamless. No full polyfill exists for native import map resolution, requiring conditional bootstrapping strategies.
The Multi-Year Adoption Problem:
Some browsers support dynamic import map updates via HTMLScriptElement.supports('importmap-dynamic') and runtime script insertion, but support isn't universal. This affects use cases like:
- A/B testing different module versions
- Feature flags that swap implementations
- Runtime plugin systems
- Gradual rollouts and canary deployments
Import map semantics present another constraint: once a module specifier resolves in a given realm, subsequent import map changes typically don't affect already-resolved modules. Many implementations ignore duplicate keys or later definitions for existing specifiers. You cannot reliably redefine an already-used specifier to swap code in-place.
// Initial import map
{
"imports": {
"app1": "https://cdn.example.com/app1-v1/index.js"
}
}
// Later attempts to update app1 may not take effect for already-resolved modules.
// New resolutions may still use the first definition depending on engine behavior.
// This constrains:
// - Hot module replacement
// - Dynamic feature loading
// - Plugin systems with install/uninstall semantics
// - Memory management strategies
Because native ESM lacks a standard mechanism to unload or reload modules, and import map rewrites face limitations, sophisticated runtime composition requires alternative strategies: iframes or workers for isolation, custom dynamic script loaders, or Module Federation-style containers.
4. Dynamic module exports and runtime flexibility
Native ESM exports are statically bound; you can't redefine an already-loaded module's exports within the same realm at runtime.
Module Federation provides first-class runtime composition with fallback patterns:
// Module Federation enables this:
const RemoteComponent = React.lazy(() =>
loadRemote('app2/Component').catch(() => loadRemote('app2Fallback/Component')),
);
// With conditional logic:
const remoteApp = userFeatureFlags.newUI ? loadRemote('app2/ComponentV2') : loadRemote('app2/ComponentV1');
With native ESM, specifier resolution is static and tied to the import graph. While import() supports conditional logic, no standard mechanism exists to swap an already-resolved module, coordinate shared singletons across conditionally loaded code, or negotiate versions at runtime. Import maps offer limited, non-portable dynamic updates, making patterns like registry-driven discovery, multi-source fallbacks, and coordinated feature-flag swaps cumbersome without higher-level orchestration.
Runtime plugin systems:
Native ESM lacks a pluggable loader in browsers. Building runtime plugin systems requires custom isolation (iframes, workers) or bespoke loaders. Module Federation provides loader-level hooks:
beforeLoadRemote: Modify remote loading behaviorerrorLoadRemote: Handle failures and define fallbacksafterResolve: Transform or alias resolutionsbeforeInit: Configure shared dependencies and versioning
These hooks enable architectures for observability (tracking remote load timings and errors), dynamic A/B or canary rollouts, graceful fallback and rollback on failure, and performance strategies (preload, lazy load, priority-based fetching) tied to user behavior.
Pure ESM provides building blocks. Module Federation supplies the composition layer: coordinated sharing, version negotiation, controlled loading, and structured fallbacks.
5. Memory leaks and dynamic remote management
Native ESM has no standard mechanism to unload modules within a browsing context; once instantiated, modules persist for the life of the realm unless the page or worker terminates.
In real applications—especially single-page applications where users maintain sessions for hours or days—memory management becomes critical. Consider TikTok, where users navigate hundreds of views in a single session. Or Slack, kept open in tabs for weeks. Or admin dashboards and monitoring systems that run continuously without page refreshes. These scenarios benefit from the ability to unload remote code, clear caches, and reclaim memory without forcing a full reload.
Module Federation provides APIs for dynamic remote lifecycle management:
import { registerRemotes } from '@module-federation/enhanced/runtime';
// As users exit views that won't be revisited:
// Re-register to replace an existing remote (uninstall + install)
// Triggers cache invalidation and cleanup of prior remote state
registerRemotes(
[
{
name: 'settingsPanel',
entry: 'https://cdn.example.com/settings-v1/mf-manifest.json',
},
],
{ force: true },
);
// Later, register a different version if needed
// The force flag triggers removeRemote() internally before re-adding v2
registerRemotes(
[
{
name: 'settingsPanel',
entry: 'https://cdn.example.com/settings-v2/mf-manifest.json',
},
],
{ force: true },
);
// Internally, removeRemote() aims to:
// - Deregister the remote from federation registries
// - Invalidate module factory/instance caches for that remote
// - Remove global entry references (e.g., containers on window)
// - Release references to shared scopes where possible
// Actual memory reclamation depends on releasing all app-held references for GC.
Large SPAs can "uninstall" infrequently used remotes—settings panels, editors, commerce modules—as users exit those areas, then reinstall on demand to cap memory growth.
With native ESM, once a module evaluates in a realm, no standard API exists to unload or re-evaluate it under a new URL. Import maps don't reliably "swap" already-resolved modules. In long-running applications, this makes active memory management difficult without isolation boundaries (iframes, workers) or custom loaders.
6. Singleton management and version negotiation
The singleton problem remains unaddressed by native ESM.
Consider this scenario with React:
// App 1 imports React 18.2.0
import React from 'https://cdn.example.com/react@18.2.0/index.js';
// App 2 imports React 18.3.0
import React from 'https://cdn.example.com/react@18.3.0/index.js';
// Result: two React runtimes in the same page.
// React Context won't flow across app boundaries.
// State sharing via hooks/stores tied to a single instance breaks.
// Event systems tied to a single runtime won't interoperate.
Module Federation solves this through automatic version negotiation:
// rsbuild.config.ts (Module Federation)
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
strictVersion: false, // allow compatible ranges at runtime
}
}
The runtime automatically:
- Detects required versions across all federated apps
- Negotiates compatible versions using semver ranges (within defined constraints)
- Ensures only one instance loads (singleton enforcement)
- Surfaces clear errors when version requirements conflict
ESM Limitation:
With native ESM, no built-in concept of singletons or semver negotiation exists. If App A imports react@18.2.0 and App B imports react@18.3.0, both load simultaneously, creating multiple React runtimes and breaking cross-app Context and state. Avoiding this requires strict operational coordination to pin identical versions across all apps—a manual, error-prone process.
The layering problem:
Advanced architectures face even more complex requirements. What happens when two dependencies share the same specifier but must resolve to different singletons depending on the layer?
Example:
// Layer 1: Admin panel uses React 18
import React from 'react'; // Should resolve to React 18
// Layer 2: Legacy widget uses React 17 (incompatible, needs separate singleton)
import React from 'react'; // Should resolve to React 17
// Both are singletons within their layer
// Both use the specifier 'react'
// But they must remain isolated from each other
This pattern emerges during gradual migrations. Module Federation supports scoped sharing and layering so different parts of an application can resolve "react" to different, isolated singletons when necessary. With native ESM and import maps, specifiers are global within a realm and path-based; no standard mechanism exists to scope the same specifier to different singletons per layer without separate realms (iframes, workers) or custom loaders.
7. Build and deployment overhead
The "no bundler" myth:
Enterprises still require bundlers even with ESM. Production deployments need code splitting for performance, tree shaking to reduce bundle size, minification and optimization, TypeScript compilation, CSS processing and extraction, environment variable injection, and source maps for debugging production issues.
You're not escaping build tooling—you're shifting where the complexity lives.
With pure ESM sharing, you typically need:
Project A (exports shared library)
├── Build for ESM
├── Deploy to CDN
└── Version management
Project B (exports shared library)
├── Build for ESM
├── Deploy to CDN
└── Version management
Project C (consumes both)
├── Manually track versions
├── Update import maps
├── Coordinate deployments
└── Handle breaking changes
This creates significant operational overhead. If you have internal packages to share, you typically need separate build pipelines to publish each package as ESM to a CDN or registry. Every application that shares a library ends up owning a build and publish pipeline for it.
With Module Federation:
Each application builds once with its own dependencies. Applications can expose libraries, and Module Federation negotiates compatible versions at runtime (within configured semver ranges). The runtime handles sharing—no separate "shared library" builds, minimal manual version coordination, and looser deployment coupling.
Tree Shaking Requires Bundlers:
A contradiction exists in the "no bundler" argument: effective tree-shaking requires static analysis performed by bundlers. Tree shaking demands parsing all exports across all applications to understand what's available, analyzing side effects to determine what's safe to remove, and performing static analysis of the dependency graph.
Pure runtime ESM loading doesn't perform this analysis. If you want optimal performance, you still need a bundler.
Module Federation supports multi-phase optimization:
- Compile-time: Emit both shaken and non-shaken chunks as available options
- Runtime negotiation: Select from those options based on what other apps actually consume
- Dynamic strategies: Frameworks like VMOK can generate "magic remotes" that tailor shares per deployment or version
Without this, you're either shipping unshaken code (wasting bandwidth), attempting costly runtime optimizations (hurting performance), or manually coordinating per-app needs (which doesn't scale).
8. CommonJS and ESM interoperability issues
The module format gap remains messy.
Many npm packages are not purely ESM. A sizable subset remains CommonJS-only; others provide dual packages (CJS + ESM) with subtle behavioral differences. CSS imports aren't standardized in native ESM for browsers; JSON and other non-JS resources vary by environment and require tooling. Default versus named export differences and dual-package hazards remain common pain points.
Example problem with CommonJS conversion:
// Package exports CommonJS
module.exports = { SomeComponent };
// ESM import of a CJS module often yields a default that wraps CJS exports:
import pkg from 'some-package'; // pkg.default?.SomeComponent in many bundler/runtime interop modes
const { SomeComponent } = pkg.default; // Awkward
// Versus the expected:
import { SomeComponent } from 'some-package';
Example problem with named versus default exports:
// You expose React as a shared dependency
// In one app, you try to destructure Suspense:
import { Suspense } from 'react';
// This can fail if the package is CJS or the interop mode doesn't synthesize named exports.
// You'd need:
import React from 'react';
const { Suspense } = React; // Works when consuming CJS via default interop
CSS as a dealbreaker:
Native ESM has no cross-browser standard for importing CSS in JS modules. In practice, CSS handling relies on bundlers (extraction, modules, ordering) or runtime loaders. Converting CJS-oriented packages to ESM surfaces quirks in CSS processing, style injection order, CSS modules semantics, and scoped styles.
Module Federation's bundler integration handles CJS/ESM interop, CSS pipelines, assets, and export map quirks transparently at build time while coordinating sharing at runtime. With pure ESM, teams must manage dual-package hazards, fragile export map configurations, default versus named export interop, CSS versus CSS-in-JS differences, asset handling, and worker or binary edge cases manually.
9. Performance implications
The download waterfall problem:
With ESM and import maps:
1. Download HTML
2. Parse HTML
3. Download import map
4. Parse import map
5. Encounter first import
6. Download module A
7. Parse module A
8. Discover it imports B, C, D
9. Download B, C, D
10. Parse and execute...
Naive ESM loading creates depth-driven waterfalls. While browsers employ preloading, HTTP/2/3 multiplexing, and import-graph heuristics, high latency and many granular modules still degrade performance.
Module Federation with proper chunking:
1. Download HTML
2. Download initial bundle (contains manifest)
3. Runtime knows full dependency graph
4. Parallel download of required chunks
5. Execute
A manifest-based approach allows the runtime to preload and parallelize based on a known graph, often more aggressively than native ESM discovery alone.
The Network Request Explosion:
With pure browser-level ESM sharing, no native code splitting or merging into larger chunks exists.
When you share React in Module Federation, lots of small dependencies React requires—react-is, scheduler, internal utilities—bundle into one optimized chunk.
With ESM, each module URL typically fetches separately unless you add a bundling layer or server-side packer:
import 'react' // Request 1
├─ import 'react-is' // Request 2
├─ import 'scheduler' // Request 3
│ ├─ import 'scheduler/tracing' // Request 4
│ └─ import 'scheduler/unstable' // Request 5
└─ import 'shared/ReactTypes' // Request 6
└─ import 'shared/ReactSymbols' // Request 7
At scale—hundreds of remotes each with dozens of shared dependencies—this can balloon into tens of thousands of individual fetches without bundling or consolidation.
Even with HTTP/2/3 multiplexing, many tiny requests increase overhead and contention. Cross-origin DNS and TLS handshakes add setup cost. Latency amplifies with request count. Without bundling, you lose cross-file compression opportunities, and fine-grained prioritization becomes harder.
Module Federation with chunking mitigates this by bundling related dependencies into optimized chunks, using a manifest to coordinate parallel downloads and preloads, prioritizing critical paths, and deduplicating shared dependencies across applications at runtime.
10. Enterprise features Module Federation provides
Beyond the technical limitations of ESM, Module Federation provides production-grade features that matter in real deployments.
Version management works automatically at runtime. Module Federation negotiates compatible versions using configured semver ranges, so you don't manually coordinate versions across teams. It supports export-level prioritization, preferring remotes that expose required symbols. When preferred versions aren't available, it falls back gracefully. Near-version matching means 18.2.0 can satisfy ^18.0.0, and you get patterns for canary rollouts built in.
Error handling is robust out of the box. You get built-in retry and backoff for remote loads, graceful degradation paths when things fail, integration with UI error boundaries to isolate failures, and configurable fallbacks when a primary remote can't load.
Performance gets serious attention. Differential loading reduces bytes by fetching only what changed between versions. Manifest-based preloading lets the runtime fetch dependencies in parallel before they're needed. Optimized chunk splitting aligns chunk sizes with caching and loading patterns. Shared dependency deduplication automatically prevents loading the same library multiple times.
Developer Experience stays smooth. Type safety works across federated modules through TypeScript federation, catching errors at build time. Hot Module Replacement works seamlessly across federated modules during development. You get framework-agnostic support for React, Vue, Angular, Svelte, and integration with major bundlers like Webpack, Rspack, Vite, and Rollup via plugins.
Monitoring and Observability come built-in. Hooks and adapters capture module load performance and usage patterns. Performance metrics identify bottlenecks in your federation architecture. Error reporting surfaces issues with remote loads in production. Version auditing helps you understand what versions are actually deployed and in use.
When Native ESM Works
To be fair, scenarios exist where a pure ESM approach remains viable:
- Prototypes and small projects with simple dependency graphs
- Public documentation sites where reliability isn't mission-critical
- Educational examples demonstrating web standards
- Internal tools used by small teams with full control over the environment
- Progressive enhancement layers on top of traditional applications for non-critical features
For production micro-frontend architectures at scale, the limitations outlined above often become dealbreakers.
Real-world scale: where are these actually used?
Let's examine deployment patterns. Where is import map-based ESM sharing actually deployed at scale, and how does it compare to Module Federation deployments?
The largest known import map deployment:
Shopify (~$130B market cap)
- Widget-style scenarios
- Sharing libraries (e.g., React) with widgets and embedded apps
- Relatively constrained scope
- Primarily merchant-facing apps and storefront extensions
Module Federation deployments:
ByteDance (~$500B market cap)
- TikTok, Douyin, Lark
- Hundreds of micro-frontends
- 70+ dependencies per remote
- 500+ remotes in production
Microsoft (~$3T market cap)
- Enterprise products across Office 365, Azure Portal, Teams
- Cannot rely on external CDNs for security and compliance
- Internal infrastructure requirements
- Multi-tenant SaaS at massive scale
Amazon (~$1.8T market cap)
- Internal tools and customer-facing applications
- AWS Console, seller and vendor portals
- Full enterprise infrastructure control
- Air-gapped deployment requirements
The Scale Gap:
The scale gap is significant. Publicly discussed import map use cases skew toward simpler widget scenarios, while Module Federation sees battle-testing in organizations operating hundreds of remotes with dozens of shared dependencies per remote.
Why the difference?
Widget-style sharing suits import maps well. But at enterprise scale—hundreds of micro-frontends, dozens of shared dependencies, SPAs running for days, dynamic plugin systems, strict security requirements, and sophisticated error recovery—you need Module Federation's runtime composition capabilities.
Industry Signal:
Many mainstream build tools prioritize federated or bundled flows over pure browser-level ESM externals, reflecting current production needs around sharing, optimization, and developer experience.
The Bottom Line
The question isn't "Can you technically build micro-frontends with ESM and import maps?"
The question is: "Can you reliably build, deploy, maintain, and scale a production micro-frontend architecture using only ESM and import maps, while handling real-world constraints like enterprise security, multi-region deployment, version management, error recovery, and developer experience?"
Based on production experience to date: not reliably—not without a federation-style runtime to handle versioning, sharing, lifecycle, and failure modes.
Conclusion
Native web standards matter. ESM, import maps, and web components represent meaningful progress for the web platform. But they're not silver bullets, and they're not enterprise-ready replacements for Module Federation yet.
Module Federation exists because it solves real production problems that standards alone don't address. When you have hundreds of micro-frontends sharing dozens of dependencies, you need automatic version negotiation at runtime—Module Federation handles this with semver ranges and fallback strategies, while ESM requires manual version alignment across teams. The same pattern repeats across every dimension that matters at scale.
For singleton management, Module Federation enforces coordination automatically so React Context flows correctly across app boundaries. With ESM, you manually coordinate to avoid loading multiple runtimes. For runtime flexibility, Module Federation provides first-class dynamic loading with fallback patterns and feature flag support. ESM offers conditional import() but no coordinated runtime composition. When things fail, Module Federation has loader-level hooks and graceful degradation built in. ESM has limited recovery options.
Performance optimization shows the same gap. Module Federation uses manifests and intelligent chunking to avoid waterfalls. ESM creates depth-driven dependency chains that hurt performance without careful bundling and preloading strategies. The developer experience difference is stark too—Module Federation scales to thousands of modules with coordinated tooling, while ESM requires significant manual coordination at that scale.
Enterprise security needs matter. Module Federation works with internal infrastructure and private registries. ESM typically depends on external origins unless you mirror everything internally. Memory management in long-running SPAs requires the ability to unload and reload remotes programmatically—Module Federation provides this, ESM doesn't have unload semantics. Even tree shaking requires different approaches: Module Federation optimizes at both compile-time and runtime, while ESM needs a bundler and tooling layer anyway.
Standards take time to mature. Production systems need solutions that work today, at scale, in real environments with real constraints. That's what Module Federation provides. The goal isn't to replace web standards—it's to build on top of them to solve complex problems that real applications face. If import maps gain robust dynamic capabilities, if ESM acquires version and singleton semantics, and as runtime support matures, the landscape could shift. Until then, for serious production micro-frontend architectures, Module Federation addresses problems that pure standards-based approaches don't yet handle.
Building with Zephyr Cloud
At Zephyr Cloud, we've built our platform on Module Federation to solve these deployment challenges. We provide instant deployments, automatic version management, multi-CDN support, and production monitoring built specifically for federated architectures.
Learn more: docs.zephyr-cloud.io