Microfrontends: cuándo tienen sentido y cuándo no
Un análisis honesto de la arquitectura de microfrontends: patrones de implementación, costes reales y los criterios que uso para decidir si adoptarlos en un proyecto.
Microfrontends: cuándo tienen sentido y cuándo no
La palabra "microfrontends" genera reacciones fuertes. Hay equipos que la adoptan como solución a todos sus males de escala, y otros que la descartan como sobreingeniería. Ambas posturas suelen ser equivocadas.
En este artículo no voy a vender la arquitectura ni a demolerla. Voy a explicar cómo funciona, qué problemas resuelve de verdad, y los criterios que aplico cuando tengo que tomar esa decisión en un proyecto.
Qué es un microfrontend
La idea viene directamente del mundo backend: si los microservicios dividen una aplicación en servicios independientes desplegables, los microfrontends hacen lo mismo con la interfaz.
Cada microfrontend es una unidad autónoma de UI: tiene su propio repositorio, su propio proceso de build, su propio ciclo de despliegue, y —crucialmente— puede estar escrita en un framework diferente al resto de la aplicación.
Una aplicación compuesta puede verse así:
┌─────────────────────────────────────────────┐
│ Shell / Host app │
├──────────────┬──────────────┬───────────────┤
│ MFE: Catálogo │ MFE: Carrito │ MFE: Checkout │
│ (Vue 3) │ (React) │ (Vue 3) │
└──────────────┴──────────────┴───────────────┘
El shell es la aplicación contenedora que carga, monta y gestiona la comunicación entre los microfrontends. Los MFEs son independientes entre sí.
Patrones de integración
Hay varias formas de componer microfrontends. Cada una tiene trade-offs muy distintos.
Module Federation (Webpack / Rspack)
El patrón más utilizado en producción hoy. Module Federation permite que una aplicación exponga módulos y que otra los consuma en tiempo de ejecución, sin bundlearlos juntos.
// webpack.config.js — MFE: Catálogo
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'catalogo',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList.vue',
},
shared: {
vue: { singleton: true, requiredVersion: '^3.0.0' },
},
}),
],
}
// webpack.config.js — Shell
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
catalogo: 'catalogo@https://catalogo.ejemplo.com/remoteEntry.js',
},
}),
],
}
Y en el shell, consumir el componente remoto es tan sencillo como:
// Carga lazy en tiempo de ejecución
const ProductList = defineAsyncComponent(() =>
import('catalogo/ProductList')
)
La clave de shared es fundamental: si Vue no está marcado como singleton, acabarás con dos instancias de Vue en la misma página, con todos los problemas que eso conlleva.
Web Components
Cada MFE se expone como un Custom Element. El shell los usa con HTML estándar.
// En el MFE: registrar el componente
class CatalogoWidget extends HTMLElement {
private _app: ReturnType<typeof createApp> | null = null
connectedCallback() {
this._app = createApp(App)
this._app.mount(this)
}
disconnectedCallback() {
this._app?.unmount()
this._app = null
}
}
customElements.define('mfe-catalogo', CatalogoWidget)
<!-- En el shell -->
<mfe-catalogo data-categoria="zapatillas" />
Ventaja: agnóstico de framework, el shell no sabe ni le importa qué hay dentro.
Inconveniente: la comunicación vía atributos y eventos DOM es más limitada, y el Shadow DOM complica compartir estilos globales.
Iframes
El enfoque más antiguo y el más aislado. Cada MFE vive en su propio iframe. La comunicación se hace via postMessage.
// Shell → MFE
const iframe = document.querySelector<HTMLIFrameElement>('#mfe-carrito')
// Siempre especifica el origen exacto, nunca '*' en producción
iframe?.contentWindow?.postMessage({ type: 'ADD_ITEM', payload: item }, 'https://carrito.ejemplo.com')
// MFE escuchando — validar siempre el origen
window.addEventListener('message', (event) => {
if (event.origin !== 'https://shell.ejemplo.com') return
if (event.data.type === 'ADD_ITEM') {
store.addItem(event.data.payload)
}
})
El aislamiento es total —CSS, JS, contexto de ejecución— pero el rendimiento y la UX sufren. Los iframes tienen sus propios contextos de scroll, su propio historial, y la accesibilidad es compleja de gestionar. Solo los usaría para integrar aplicaciones legacy que no puedo tocar.
El coste real que nadie menciona
Los artículos de arquitectura suelen hablar de ventajas. Estos son los costes que me encuentro en la práctica:
1. Overhead de red
Cada MFE carga su propio runtime. Si el shell usa Vue 3 y el MFE de carrito también usa Vue 3 pero sin shared bien configurado, el usuario descarga Vue dos veces. Con cinco MFEs eso escala mal.
La solución son las dependencias compartidas, pero gestionarlas correctamente —con versiones compatibles entre equipos que evolucionan independientemente— es una fuente constante de conflictos.
2. Estado compartido
¿Cómo comparten estado dos MFEs que no se conocen entre sí? Las opciones son:
- URL como fuente de verdad — el estado vive en query params o fragmentos
- Custom events en el DOM — el shell hace de bus de eventos
- Store compartido — solo funciona si ambos MFEs usan el mismo framework y versión
Ninguna es tan limpia como un store reactivo normal dentro de una SPA.
3. Experiencia de desarrollo fragmentada
Con una monorepo bien configurada se mitiga, pero sigue siendo más complejo que una SPA. Los desarrolladores necesitan levantar múltiples proyectos localmente, gestionar versiones de contratos entre equipos, y coordinarse en cambios que afectan al shell.
4. Consistencia visual
Si cada equipo puede elegir libremente sus componentes, el design system se fragmenta. La solución —un paquete de componentes compartido— introduce otra dependencia que hay que versionar y que puede convertirse en un cuello de botella.
Cuándo los adopto (y cuándo no)
Después de varios proyectos con esta arquitectura, he llegado a un criterio bastante claro.
Tiene sentido cuando:
- Hay múltiples equipos autónomos que necesitan desplegar de forma independiente sin coordinación constante
- La aplicación tiene áreas con ciclos de vida muy distintos (el checkout se despliega 10 veces al día; el CMS, una vez al mes)
- Se está migrando una aplicación legacy y no se puede reescribir todo a la vez
- La organización ya tiene microservicios en backend y los equipos están alineados en torno a dominios de negocio
No tiene sentido cuando:
- Hay un solo equipo — el coste operativo supera cualquier beneficio
- La aplicación es pequeña o mediana — una SPA bien estructurada con lazy loading hace el mismo trabajo
- Se adopta por moda arquitectónica en lugar de por un problema concreto a resolver
- El equipo no tiene experiencia gestionando infraestructura distribuida
La pregunta que siempre hago primero: ¿qué problema específico de mi organización resuelve esto? Si la respuesta es vaga, la respuesta es no.
Una alternativa que suele funcionar mejor
Antes de saltar a microfrontends, siempre evalúo una monolito modular con lazy loading agresivo:
// router/index.ts — Vue Router con chunks separados
const routes = [
{
path: '/catalogo',
component: () => import('@/modules/catalogo/CatalogoView.vue'),
},
{
path: '/carrito',
component: () => import('@/modules/carrito/CarritoView.vue'),
},
{
path: '/checkout',
component: () => import('@/modules/checkout/CheckoutView.vue'),
},
]
Con Vite y code splitting, cada módulo genera su propio chunk. Los usuarios no descargan el código de checkout hasta que lo necesitan. Los equipos pueden trabajar en módulos independientes dentro del mismo repositorio. El estado compartido es trivial.
Este enfoque resuelve el 80% de los problemas que la gente intenta resolver con microfrontends, con un 20% de la complejidad operativa.
El problema que nadie menciona primero: el routing
El estado compartido es complejo. El routing es peor. Los artículos de microfrontends suelen ignorarlo.
¿Quién controla la URL? ¿El shell o cada MFE? Las opciones son:
Routing en el shell: el shell decide qué MFE se monta en función de la ruta, y cada MFE recibe su subruta como prop. Funciona bien cuando los MFEs son fragmentos de página (un widget, un panel lateral).
Routing distribuido: cada MFE tiene su propio router y gestiona su segmento de URL. Más autonomía, pero hay que sincronizar el historial del navegador entre aplicaciones. Con Vue Router puedes usar createMemoryHistory() en los MFEs y dejar que el shell maneje createWebHistory().
// En el MFE — modo memoria, sin control de URL
const router = createRouter({
history: createMemoryHistory(),
routes: [...]
})
// El shell comunica cambios de ruta al MFE vía evento
window.dispatchEvent(new CustomEvent('mfe:navigate', { detail: { path: '/checkout/confirmation' } }))
No hay una solución limpia. Si el proyecto llega a este nivel de complejidad de routing entre MFEs, es una señal de que los dominios de negocio no están bien delimitados.
Module Federation también funciona con Vite
Los ejemplos anteriores usan Webpack porque es donde Module Federation nació (Webpack 5, 2020). Si usas Vite, tienes dos opciones:
@originjs/vite-plugin-federation: implementación community de Module Federation para Vite. Madura, usada en producción.- Rspack: bundler compatible con Webpack, con Module Federation nativo y velocidad comparable a Vite. La opción que elegiría para nuevos proyectos con MFEs.
// vite.config.ts — con vite-plugin-federation
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
federation({
name: 'catalogo',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList.vue',
},
shared: ['vue'],
}),
],
build: { target: 'esnext' },
})
La API es casi idéntica a la de Webpack. La diferencia relevante: build.target: 'esnext' es obligatorio — los módulos federados usan dynamic imports de ES y no funcionan si el build apunta a CommonJS.
Conclusión
Los microfrontends son una herramienta válida para organizaciones grandes con equipos múltiples y dominios bien definidos. En ese contexto, Module Federation es el patrón más maduro y pragmático disponible hoy.
Fuera de ese contexto, son sobreingeniería. Y la sobreingeniería tiene un coste real: más tiempo en infraestructura, menos en producto.
Como con casi todo en arquitectura de software: la respuesta correcta depende del problema concreto, no de lo que está de moda en Twitter.
Si estás evaluando esta decisión en tu proyecto y quieres contrastarlo, escríbeme a hola@miguel-jimenez.dev.