Vinicius Aguiar
Frontend

Hay una app Angular dentro de este post en Next.js

24 de sept. de 2026 · 6 min de lectura

Quería entender los micro frontends construyendo uno, no leyendo sobre ellos. Así que me puse una restricción: el experimento tenía que vivir dentro de una página real de este sitio, y la propia página tenía que ser la prueba. Este post es esa página.

Este sitio es Next.js 16 con React 19. El panel de abajo no lo es. Es una app Angular 22, con su propio repositorio, su propio build y su propio deploy, cargada en tiempo de ejecución desde otro dominio. Aparte de una URL, nada en el build de este sitio sabe que existe.

Cargando el micro frontend Angular…

Todavía no hay eventos del micro frontend.

Pruébalo: cambia el tema o el idioma del sitio, y el panel lo sigue. Haz clic en su botón, y la línea de abajo, que pertenece al sitio y no a Angular, registra el evento. Todo en el panel se lee en tiempo de ejecución: la versión de Angular, el dominio del que vino, el commit con el que se construyó y cuánto costó descargarlo.

Tres formas de componer un frontend

No existe un único estándar de micro frontends. Hay tres comunes, y resuelven problemas distintos.

  • División por ruta. Cada área del producto es una app separada, y un proxy o el edge envía cada ruta a la app correcta. En Next.js esto se llama multi-zones. Es simple de operar; el costo es una carga completa de página cada vez que pasas de una app a otra.
  • Module Federation. El host carga módulos de otros deploys en tiempo de ejecución y comparte dependencias con ellos. En el mundo Angular, Native Federation es la opción de facto. Brilla cuando todas las piezas usan el mismo framework.
  • Web Components. El micro frontend se empaqueta como custom element, y el host solo tiene que renderizar una etiqueta. Es la única interfaz que React y Angular entienden de forma nativa.

Aquí el host es React y el micro frontend es Angular, así que es composición entre frameworks. En ese caso los Web Components son la respuesta habitual, y justamente para eso existe Angular Elements.

El contrato

La forma más útil que encontré de pensar un micro frontend es como un contrato: todo lo que un lado sabe del otro. Aquí son tres cosas.

**Las entradas son locale y theme.** Este sitio las define como propiedades del elemento; una página HTML simple puede definirlas como atributos, y Angular Elements mapea ambas. El panel tiene su propio diccionario para los cinco idiomas que soporta este sitio y sus propios colores para cada tema. Nada se hereda del CSS del sitio.

La salida es un evento DOM. El panel no sabe que React existe. Dispara un CustomEvent, y el sitio lo escucha:

this.host.nativeElement.dispatchEvent(
  new CustomEvent("mfe:ping", {
    detail: { angularVersion: this.angularVersion, at: new Date().toISOString() },
    bubbles: true,
    composed: true,
  }),
)

El aislamiento es Shadow DOM. Los estilos del panel no se filtran a la página, y el Tailwind del sitio no se filtra al panel.

Del lado de Angular, todo el micro frontend se reduce a registrar una etiqueta:

import { provideZonelessChangeDetection } from "@angular/core"
import { createApplication } from "@angular/platform-browser"
import { createCustomElement } from "@angular/elements"
import { InspectorComponent } from "./app/inspector.component"

export const TAG = "mfe-inspector"

export async function registerInspector(): Promise<void> {
  if (customElements.get(TAG)) return
  const app = await createApplication({ providers: [provideZonelessChangeDetection()] })
  // Checked again: two evaluations of this file can both pass the first check.
  if (customElements.get(TAG)) return
  customElements.define(TAG, createCustomElement(InspectorComponent, { injector: app.injector }))
}

Del lado del sitio, cargarlo significa importar una URL que el bundler no puede tocar y luego esperar a que la etiqueta esté definida:

loadRemoteModule(async () => {
  // The bundler must not resolve this: the URL belongs to another deploy.
  await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ src)
  await customElements.whenDefined(tag)
})

Lo que costó

Estos números se midieron sobre el archivo publicado, no se estimaron:

  • El panel se entrega en un único archivo de 119 KB, o 42 KB comprimido. Para un panel con un título, una lista corta y un botón, ese es el precio de llevar un segundo framework a la página.
  • Con mi conexión, la mediana de cinco cargas en frío fue de 44 ms para descargar el archivo y 50 ms hasta que el elemento estuvo listo para usarse.
  • Si el archivo no carga, el sitio muestra un aviso corto en lugar del panel en aproximadamente un tercio de segundo, y el resto del post sigue funcionando. Para el caso en que el servidor nunca responde, hay un timeout de 8 segundos.

El archivo solo se pide cuando el panel se acerca a la pantalla, así que quien nunca llega hasta aquí nunca paga por Angular.

Lo que los tutoriales se saltan

La mayor parte del trabajo no fue el componente. Fueron los bordes.

  • Confiar en código de otro dominio. El sitio ahora ejecuta JavaScript que no construyó. Los datos del post solo aceptan un micro frontend que venga de un origen en la lista de permitidos, por HTTPS; cualquier otra cosa se descarta antes de que la página se renderice.
  • Un archivo, a propósito. El sitio importa una única URL. Si algún día el build de Angular se divide en chunks, ese contrato se rompe en silencio, así que el build falla ruidosamente en su lugar.
  • Registrar dos veces. Esperaba que salir de la página y volver ejecutara el módulo otra vez y lanzara un error en la segunda definición de la etiqueta. No pasa: el navegador evalúa un módulo una vez por URL. Lo que sí lanza error es el mismo archivo cargado desde dos URLs, y ahí una comprobación simple tampoco alcanzaba, porque las dos evaluaciones la pasan antes de que cualquiera registre. Así que la comprobación se repite después de que Angular arranca.
  • Medir entre orígenes. Sin el header Timing-Allow-Origin, el navegador informa 0 bytes para archivos de otros dominios, y el panel no podría distinguir su propio peso de una copia en caché. Una revalidación 304 tiene la misma trampa: solo viajan los headers, así que el panel dice caché en lugar de afirmar que el módulo pesa 0,3 KB.
  • Deploy independiente. El sitio carga el archivo en tiempo de ejecución, así que un nuevo deploy del micro frontend llega a este post sin reconstruir el sitio. Lo probé publicando un cambio visible solo en el panel. La caché corta (60 segundos, más hasta 5 minutos sirviendo la versión anterior mientras revalida) solo limita cuánto tiempo quien vuelve puede ver la antigua.

Cuándo no vale la pena

Para este sitio, un micro frontend es excesivo, y ese era el punto: quería conocer el costo antes de necesitar pagarlo. Un segundo framework, un segundo pipeline, un contrato que mantener sincronizado y código de otro dominio ejecutándose en la página. Con un solo equipo y una sola base de código, un monolito bien organizado da casi todos los beneficios por una fracción de eso.

Empieza a valer la pena cuando el problema son las personas, no el código: varios equipos, cada uno con su propio ritmo de release, pisándose en el mismo frontend. Este sitio no tiene ese problema. Ahora sé cuánto cuesta resolverlo.

El código del micro frontend es público: github.com/ViniAguiar1/mfe-angular-inspector.