<!-- Canonical: https://lucioduran.com/es/blog/micro-frontends-module-federation-native-federation
Language: es
Content-SHA256: 1ea8d7ad3978833723606947c964cf8a70c180fbd8bde5640be4d572de3db83f
-->

Frontend

# Micro-Frontends en 2026: Module Federation 2.0, Native Federation e Import Maps

2025-11-028 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/micro-frontends-module-federation-native-federation>)

## La Decisión Arquitectónica

Tres enfoques merecen evaluación:

1. **Module Federation 2.0** (via Rspack), Compartir módulos en runtime, ecosistema maduro, específico de webpack/rspack
2. **Native Federation**, ES modules + import maps, agnóstico de bundler, más nuevo
3. **Composición en build-time** (paquetes npm), Cada MFE publicado como paquete npm, compuesto en build time

[Module Federation](<https://module-federation.io/>) 2.0 con Rspack para la aplicación core e import maps como capa de discovery representa la combinación más sólida.

### Module Federation 2.0: Qué Cambió

MF 2.0 (shipeado con Rspack 1.x y webpack 5.9x) introdujo tres cosas que lo hicieron realmente usable para producción:

**1. Protocolo de manifest.** En vez de hardcodear URLs de remotes en la config de webpack, los remotes publican un ` mf-manifest.json ` que describe sus módulos expuestos, dependencias compartidas, y entry points:

```json
{
 "id": "checkout-mfe",
 "name": "checkout",
 "metaData": {
 "buildHash": "a3f8c2d",
 "version": "2.14.0",
 "framework": "react@18.3.1"
 },
 "shared": [
 {
 "name": "react",
 "version": "18.3.1",
 "scope": "default",
 "strategy": "loaded-first"
 }
 ],
 "exposes": [
 {
 "path": "./CheckoutFlow",
 "name": "CheckoutFlow",
 "assets": {
 "js": { "async": ["checkout-flow.js"], "sync": [] },
 "css": { "async": ["checkout-flow.css"], "sync": [] }
 }
 }
 ]
}
```

**2. Runtime plugins.** Puede interceptar todo el ciclo de vida de federation, antes de cargar un remote, después de resolver dependencias compartidas, on error. Así se puede implementar A/B testing a nivel de micro-frontend:

```javascript
// host/federation-plugin.js
import { FederationRuntimePlugin } from '@module-federation/runtime';

const ABTestPlugin = () => ({
 name: 'ab-test-plugin',
 beforeRequest(args) {
 const { id, options } = args;
 const variant = getABVariant(id);
 if (variant) {
 options.remotes = options.remotes.map(remote => {
 if (remote.name === id) {
 return {
 ...remote,
 entry: remote.entry.replace(
 'mf-manifest.json',
 `mf-manifest-${variant}.json`
 )
 };
 }
 return remote;
 });
 }
 return args;
 }
});
```

**3. Generación de tipos.** MF 2.0 genera declaraciones TypeScript para módulos expuestos y las sincroniza entre micro-frontends via un manifest. No más ` // @ts-ignore ` en cada dynamic import.

### La Aplicación Host

El host (shell) es mínimo, maneja routing, autenticación, y el chrome de navegación. Cada ruta mapea a un micro-frontend remoto:

```javascript
// host/src/App.jsx
import { lazy, Suspense } from 'react';
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import { Routes, Route } from 'react-router-dom';

init({
 name: 'host',
 remotes: [
 { name: 'dashboard', entry: `${window.__MFE_CONFIG__.dashboard}/mf-manifest.json` },
 { name: 'checkout', entry: `${window.__MFE_CONFIG__.checkout}/mf-manifest.json` },
 { name: 'accounts', entry: `${window.__MFE_CONFIG__.accounts}/mf-manifest.json` },
 { name: 'settings', entry: `${window.__MFE_CONFIG__.settings}/mf-manifest.json` }
 ],
 shared: {
 react: { singleton: true, requiredVersion: '^18.3.0' },
 'react-dom': { singleton: true, requiredVersion: '^18.3.0' },
 'react-router-dom': { singleton: true, requiredVersion: '^6.20.0' }
 }
});

const Dashboard = lazy(() => loadRemote('dashboard/DashboardApp'));
const Checkout = lazy(() => loadRemote('checkout/CheckoutFlow'));
const Accounts = lazy(() => loadRemote('accounts/AccountsApp'));
const Settings = lazy(() => loadRemote('settings/SettingsApp'));

function App() {
 return (
 <Shell>
 <Suspense fallback={<MFELoadingSkeleton />}>
 <Routes>
 <Route path="/dashboard/*" element={<Dashboard />} />
 <Route path="/checkout/*" element={<Checkout />} />
 <Route path="/accounts/*" element={<Accounts />} />
 <Route path="/settings/*" element={<Settings />} />
 </Routes>
 </Suspense>
 </Shell>
 );
}
```

El objeto ` window.__MFE_CONFIG__ ` lo inyecta el pipeline de deployment. En producción, apunta a URLs de CDN. En desarrollo, los desarrolladores pueden overridear entries específicas para apuntar a ` localhost:3001 `, ` localhost:3002 `, etc.

## Import Maps como Capa de Discovery

Acá encajan los import maps. En vez de hardcodear URLs de MFEs en el bundle JavaScript del host, servimos un import map desde un CDN que el host lee al arrancar:

```html
<script type="importmap">
{
 "imports": {
 "dashboard-mfe": "https://cdn.example.com/mfe/dashboard/v2.14.0/",
 "checkout-mfe": "https://cdn.example.com/mfe/checkout/v3.2.1/",
 "accounts-mfe": "https://cdn.example.com/mfe/accounts/v1.8.0/",
 "settings-mfe": "https://cdn.example.com/mfe/settings/v2.0.0/"
 }
}
</script>
```

Este import map lo genera el pipeline de CI cada vez que un MFE deployea. La ventaja crítica: es posible actualizar qué versión de un MFE se sirve sin redeployar la aplicación host. ¿Deployeaste checkout v3.2.2? Solo actualizá el import map. ¿Rollback? Actualizá el import map de nuevo. El host nunca cambia.

## Estado Compartido: El Problema Difícil

Acá es donde la mayoría de las arquitecturas micro-frontend se caen a pedazos. Cuatro equipos, siete MFEs, y todos necesitan saber quién es el usuario actual, cuáles son sus permisos, y si el carrito tiene items.

La tentación es compartir un store Redux. Esto es desaconsejable. Un store compartido crea acoplamiento implícito entre MFEs, el equipo A cambia un reducer, los selectors del equipo B se rompen, y nadie lo atrapa hasta producción porque deployean independientemente.

En su lugar, un enfoque de tres capas funciona bien:

### Capa 1: Contexto de Autenticación (Del Host)

El host es dueño del estado de autenticación y lo expone via un event bus custom:

```javascript
// host/src/auth-bus.js
class AuthBus {
 #listeners = new Set();
 #currentUser = null;

 setUser(user) {
 this.#currentUser = Object.freeze(user);
 this.#notify('user-changed', this.#currentUser);
 }

 getUser() { return this.#currentUser; }

 subscribe(callback) {
 this.#listeners.add(callback);
 if (this.#currentUser) {
 callback({ type: 'user-changed', payload: this.#currentUser });
 }
 return () => this.#listeners.delete(callback);
 }

 #notify(type, payload) {
 const event = Object.freeze({ type, payload });
 this.#listeners.forEach(cb => cb(event));
 }
}

window.__AUTH_BUS__ = new AuthBus();
```

### Capa 2: Eventos Cross-MFE (BroadcastChannel)

Para eventos que cualquier MFE puede producir y cualquier otro consumir (como "item agregado al carrito"), usamos ` BroadcastChannel `:

```javascript
// shared-lib/src/mfe-events.js
const channel = new BroadcastChannel('mfe-events');

export function emitMFEEvent(type, payload, source) {
 channel.postMessage({ type, payload, source, timestamp: Date.now() });
}

export function onMFEEvent(type, callback) {
 const handler = (event) => {
 if (event.data.type === type) callback(event.data);
 };
 channel.addEventListener('message', handler);
 return () => channel.removeEventListener('message', handler);
}
```

### Capa 3: URL como Estado (Router-Driven)

El estado de navegación compartido vive en la URL. Si el dashboard necesita linkear a una cuenta específica, navega a ` /accounts/ACC-123 `. El MFE de accounts lee sus route params. Sin estado compartido necesario.

## La Migración: Patrón Strangler Fig

El monolito no se reescribió. Se estranguló. Acá va un timeline representativo:

**Mes 1-2**: Levantar el host shell, infraestructura de Module Federation, y pipeline de CI/CD. El host renderizaba el monolito entero como un solo remote "legacy".

**Mes 3-4**: Se extrajo la página de settings (menor riesgo, menos dependencias) a su propio MFE. Dos ingenieros, dos semanas de código real. El resto fue plomería de CI/CD.

**Mes 5-6**: Se extrajo checkout (mayor valor de negocio, más complejo). Esto requirió la arquitectura de estado compartido descrita arriba. Tres ingenieros, seis semanas.

**Mes 7-10**: Se extrajeron dashboard y accounts en paralelo. A esa altura, los patrones estaban establecidos y cada extracción tomó 3-4 semanas por equipo.

**Mes 11-12**: Se decomisionó el remote del monolito legacy. Lo que quedó fue el host shell + cuatro MFEs independientes.

### Errores Comunes

1. **Aislamiento CSS.** Sin aislamiento CSS enforceado desde el día uno, los estilos globales del MFE A se filtran al MFE B. La solución: Shadow DOM para MFEs nuevos y un plugin PostCSS que scopea todos los selectores para código legacy.
2. **Drift de versiones de dependencias compartidas.** En tres meses, dos MFEs estaban en React 18.3.1 y dos en 18.2.0. El singleton mode de Module Federation hizo que los hooks de un equipo fallaran silenciosamente. Agregar un check de CI que falle si las versiones de dependencias compartidas divergen más de un minor previene esto.
3. **Error boundaries.** Cuando un MFE remoto falla al cargar (caída de CDN, problema de red), todo el host crasheaba porque no wrapeamos ` loadRemote() ` en error boundaries apropiados. Ahora cada punto de montaje de MFE tiene un ` <MFEErrorBoundary> ` que muestra una UI de retry.

## Impacto en Performance

Métricas clave de performance:

| Métrica | Monolito | Micro-Frontends | Delta |
| --- | --- | --- | --- |
| Bundle JS inicial | 2.4MB (gzipped 680KB) | 320KB host + MFEs lazy | -53% carga inicial |
| Time to Interactive | 4.2s | 1.8s (host) + 0.6s (primer MFE) | -43% |
| Tiempo de deploy | 22 min (build + E2E completo) | 3-4 min por MFE | -82% |
| Frecuencia de deploy | 2-3/día (todos los equipos combinados) | 3-5/día por equipo | +400% |

La carga inicial mejoró porque los usuarios solo descargan el código de la página que visitan. Los 1.8MB de librerías de charting del equipo de dashboard no se descargan hasta que alguien visita ` /dashboard `.

El tradeoff: hay un breve estado de loading al navegar entre MFEs por primera vez mientras el bundle remoto se descarga. Precargar el MFE más probable siguiente basándose en patrones de navegación del usuario, lo que mitiga esto para el 80% de las transiciones.

## Cuándo Tienen Sentido los Micro-Frontends

Los micro-frontends se justifican solo para organizaciones con la estructura de equipo que lo demande. Los micro-frontends son una solución organizacional a un problema organizacional. Con un equipo, un monolito es lo indicado. Con dos equipos, un monorepo con code ownership. Con cuatro o más equipos que necesitan cadencias de deploy independientes, los micro-frontends valen la complejidad.

La tecnología finalmente está lo suficientemente madura como para que el dolor de implementación sea manejable. La guía: arrancar con un monolito. Extraer cuando el dolor organizacional lo demande. Y al extraer, usar el patrón strangler fig. Es el único approach de migración que te deja shipear features a los usuarios cada semana mientras la descomposición pasa en el fondo.

## Preguntas frecuentes

### ¿Cuál es la diferencia entre Module Federation y Native Federation?

Module Federation es un plugin de webpack/rspack que habilita compartir módulos en runtime entre aplicaciones buildeadas separadamente. Native Federation logra lo mismo pero usando ES modules nativos del navegador e import maps en vez de un runtime específico del bundler, haciéndolo agnóstico de framework y bundler.

### ¿Debería usar micro-frontends para mi proyecto?

Solo si teners múltiples equipos que necesitan deployar cambios de frontend independientemente. Para un solo equipo o una aplicación chica, los micro-frontends agregan complejidad significativa con beneficio mínimo. El punto óptimo es 3+ equipos, 50k+ líneas de código frontend, y una cadencia de deploy que varía por equipo.

### ¿Cómo manejan los micro-frontends dependencias compartidas como React?

Module Federation 2.0 lo maneja via su configuración shared, que negocia versiones compatibles en runtime. Native Federation usa import maps para apuntar todos los micro-frontends a la misma URL de dependencia singleton. Ambos enfoques funcionan pero tienen modos de falla diferentes cuando las versiones divergen.

## Tips

1. **Tip:** Nunca compartas React via la config ` shared ` de Module Federation con ` singleton: true ` a menos que TODOS tus remotes estén en la misma versión exacta de React. Un mismatch de versión con singleton mode causa fallas silenciosas de hooks que son casi imposibles de debuggear en producción.
2. **Tip:** Utilizar import maps como tu capa de discovery de micro-frontends aunque uses Module Federation para la carga. Overrideá el import map en staging para apuntar remotes específicos a localhost para el loop de desarrollo más rápido posible.
3. **Tip:** Para estado compartido entre micro-frontends, utilizar la API ` BroadcastChannel ` del navegador para loose coupling en vez de un store Redux compartido. Cada MFE es dueño de su propio estado, la comunicación cross-MFE pasa por eventos, no por memoria compartida.

- micro-frontends
- module-federation
- native-federation
- import-maps
- webpack
- rspack
- estado-compartido
- arquitectura-frontend

[Volver al Blog](<https://lucioduran.com/es/blog>)
