En ecosistemas empresariales con decenas de equipos de producto distribuidos, los repositorios monolíticos de SPA se convierten inevitablemente en cuellos de botella críticos: los tiempos de compilación en CI/CD se degradan a decenas de minutos, los ciclos de despliegue se bloquean por dependencias cruzadas y el escalado del código genera continuos conflictos de fusión en Git.
La arquitectura de microfrontends resuelve este desafío descomponiendo una interfaz monolítica en subsistemas independientes que se desarrollan y despliegan de forma autónoma. Históricamente, los intentos de aislamiento adolecían de deficiencias insalvables:
La tecnología Module Federation permite la integración fluida de módulos remotos en tiempo de ejecución sin necesidad de recompilar la aplicación contenedora.
La especificación Module Federation v2 (implementada en el ecosistema Rspack / Rsbuild mediante @module-federation/enhanced) eleva la arquitectura a un nuevo nivel de rendimiento empresarial:
@module-federation/runtime) para interceptar la carga de fragmentos (chunks), inyectar cabeceras de autorización y facilitar el descubrimiento dinámico de servicios (Service Discovery).En la arquitectura de Module Federation, las aplicaciones asumen dos roles fundamentales:
Una misma aplicación puede operar simultáneamente como Host y como Remote (federación bidireccional).
┌──────────────────────────────────────────────────────────────────────────────────┐
│ HOST APPLICATION (Shell) │
│ http://app.enterprise.com │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ Global Context Providers: AuthContext, QueryClientProvider, ThemeProvider │ │
│ └──────────────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────▼─────────────────────────────────────┐ │
│ │ Module Federation v2 Runtime │ │
│ │ (Singleton Registry: [email protected], [email protected]) │ │
│ └──────────────────┬───────────────────────────────────┬─────────────────────┘ │
│ │ │ │
└─────────────────────┼───────────────────────────────────┼────────────────────────┘
│ (fetch remoteEntry.js) │ (fetch remoteEntry.js)
▼ ▼
┌───────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ REMOTE 1: Analytics Dashboard │ │ REMOTE 2: Billing & Checkout │
│ http://analytics.cdn.internal │ │ http://billing.cdn.internal │
│ Exposes: './DashboardWidget' │ │ Exposes: './CheckoutFlow' │
│ Shared: react, react-dom │ │ Shared: react, react-dom │
└───────────────────────────────────────┘ └──────────────────────────────────────┘El principal reto de un entorno distribuido consiste en evitar la duplicación de librerías dentro del Shared Scope (Ámbito compartido). Si el Host y un Remote cargan copias independientes de react o react-dom, React 19 arrojará un error fatal de contexto de hooks (Invalid hook call: Multiple React instances detected).
Creación del servicio Remote utilizando @rsbuild/plugin-react y @module-federation/rsbuild-plugin.
Definición de puntos de entrada de componentes y asignación de react y react-dom como singletons estrictos.
Compilación del paquete de producción que produce metadatos deterministas de versiones y fragmentos.
Carga de artefactos del Remote en un bucket de almacenamiento independiente con cabeceras CORS adecuadas.
Importación dinámica del módulo remoto mediante React.lazy con aislamiento de fallos.
Rsbuild se apoya en el motor en Rust Rspack y ofrece compatibilidad total y optimizada con Module Federation v2:
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig({
plugins: [
pluginReact(),
pluginModuleFederation({
name: 'analytics_remote',
filename: 'remoteEntry.js',
// Componentes expuestos a las aplicaciones anfitrionas
exposes: {
'./DashboardWidget': './src/widgets/DashboardWidget.tsx',
'./MetricsSummary': './src/widgets/MetricsSummary.tsx',
},
// Gestión de dependencias compartidas como singletons
shared: {
react: {
singleton: true,
requiredVersion: '^19.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^19.0.0',
},
},
}),
],
server: {
port: 3001,
},
});import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig({
plugins: [
pluginReact(),
pluginModuleFederation({
name: 'host_shell',
// Declaración de microfrontends remotos mediante su manifiesto
remotes: {
analytics_remote: 'analytics_remote@http://localhost:3001/mf-manifest.json',
},
shared: {
react: {
singleton: true,
eager: true, // El Host requiere React de forma síncrona en el arranque
requiredVersion: '^19.0.0',
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^19.0.0',
},
},
}),
],
server: {
port: 3000,
},
});Para garantizar importaciones seguras con verificación de tipos dentro de la aplicación Host, se definen declaraciones ambientales de módulos:
declare module 'analytics_remote/DashboardWidget' {
import type { FC } from 'react';
export interface DashboardWidgetProps {
tenantId: string;
refreshIntervalMs?: number;
onMetricSelect?: (metricId: string) => void;
}
const DashboardWidget: FC<DashboardWidgetProps>;
export default DashboardWidget;
}Interrupciones en la red, tiempos de espera de la CDN o errores 404/500 al descargar remoteEntry.js nunca deben provocar la caída de la aplicación Shell principal. Cada microfrontend debe aislarse tras un Error Boundary resistente:
El usuario accede a /analytics. React Router monta el componente Lazy contenedor.
El entorno solicita mf-manifest.json desde la CDN remota y valida los hashes de dependencias.
El módulo remoto se vincula al singleton de React 19 del Host sin descargar nuevamente el núcleo.
Ante fallos de red o errores de ejecución, el Host presenta una interfaz local de respaldo.
¿Qué ventaja fundamental aporta mf-manifest.json en Module Federation v2 frente a la carga directa de remoteEntry.js en v1?
A continuación se muestra un componente de carga para módulos remotos en producción que integra React 19 Suspense, un ErrorBoundary personalizado, reintentos exponenciales automáticos ante contingencias de CDN y propagación tipada de propiedades:
import React, { Component, Suspense, lazy, type ReactNode } from 'react';
interface ErrorBoundaryProps {
fallback: (error: Error, reset: () => void) => ReactNode;
children: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class MicrofrontendErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
public override state: ErrorBoundaryState = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
public override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
console.error('[Microfrontend Crash Alert]:', error, errorInfo);
}
private handleReset = (): void => {
this.setState({ hasError: false, error: null });
};
public override render(): ReactNode {
if (this.state.hasError && this.state.error) {
return this.props.fallback(this.state.error, this.handleReset);
}
return this.props.children;
}
}
// Utilidad de importación dinámica con reintento automático ante fallos de red en CDN
export function retryDynamicImport<T>(
importFn: () => Promise<T>,
retriesLeft = 3,
intervalMs = 1000
): Promise<T> {
return new Promise((resolve, reject) => {
importFn()
.then(resolve)
.catch((error: unknown) => {
if (retriesLeft <= 1) {
reject(error instanceof Error ? error : new Error(String(error)));
return;
}
setTimeout(() => {
retryDynamicImport(importFn, retriesLeft - 1, intervalMs * 1.5)
.then(resolve)
.catch(reject);
}, intervalMs);
});
});
}
interface FederatedLoaderProps<TProps extends Record<string, unknown>> {
load: () => Promise<{ default: React.ComponentType<TProps> }>;
props: TProps;
moduleName: string;
fallbackUi?: ReactNode;
}
export function FederatedComponentLoader<TProps extends Record<string, unknown>>({
load,
props,
moduleName,
fallbackUi,
}: FederatedLoaderProps<TProps>): React.JSX.Element {
const LazyComponent = React.useMemo(() => {
return lazy(() => retryDynamicImport(load));
}, [load]);
return (
<MicrofrontendErrorBoundary
fallback={(error, reset) => (
<div className="p-6 rounded-xl border border-danger/30 bg-danger/5 text-text-primary flex flex-col gap-3">
<div className="flex items-center gap-2 text-danger font-bold">
<span>⚠️ Error al cargar el microfrontend: {moduleName}</span>
</div>
<p className="text-sm text-text-muted">{error.message}</p>
<button
type="button"
onClick={reset}
className="self-start px-4 py-2 rounded-lg bg-bg-elevated border border-border hover:border-accent-cyan text-xs font-semibold text-accent-cyan transition-colors"
>
Reintentar conexión remota
</button>
</div>
)}
>
<Suspense
fallback={
fallbackUi || (
<div className="p-8 rounded-xl border border-border bg-bg-surface flex items-center justify-center">
<div className="flex items-center gap-3 text-accent-cyan text-sm font-mono animate-pulse">
<span className="w-2 h-2 rounded-full bg-accent-cyan" />
Inicializando servicio remoto {moduleName}...
</div>
</div>
)
}
>
<LazyComponent {...props} />
</Suspense>
</MicrofrontendErrorBoundary>
);
}Si las configuraciones de Host y Remote no definen singleton: true para react y react-dom, cada Remote cargará su propia instancia de React en memoria. Esto desvincula la Context API de React (devolviendo valores predeterminados) y provoca excepciones en los hooks.
// ❌ Antipatrón: la falta de singleton duplica instancias del núcleo de React
shared: {
react: { requiredVersion: '^19.0.0' },
'react-dom': { requiredVersion: '^19.0.0' },
}
// ✅ Patrón de producción: singleton estricto con validación semántica de versiones
shared: {
react: {
singleton: true,
requiredVersion: '^19.0.0',
strictVersion: true,
},
'react-dom': {
singleton: true,
requiredVersion: '^19.0.0',
strictVersion: true,
},
}Si el Host y los Remotes emplean CSS global tradicional o configuraciones de Tailwind CSS sin aislamiento de espacios de nombres (prefijos), los estilos de un módulo remoto sobrescribirán la interfaz de la aplicación principal.
/* ❌ Antipatrón: selector global de botón dentro de un Remote */
button {
background-color: #ef4444; /* ¡Modificará todos los botones de la aplicación anfitriona! */
}
/* ✅ Solución: Prefijo personalizado de Tailwind v4 o CSS Modules con ámbito local */
@theme prefix(mf-analytics) {
/* Todas las utilidades quedan aisladas bajo este espacio de nombres */
}Si la aplicación Host arranca de forma síncrona mientras React está declarado como módulo compartido asíncrono sin eager: true, la ejecución fallará inmediatamente con Shared module is not available for eager consumption.
Solución: Utilizar el patrón Bootstrap (import('./bootstrap')) en el punto de entrada o configurar React en el Host con eager: true.