Eu queria entender micro frontends construindo um, não lendo sobre eles. Então me dei uma restrição: o experimento tinha que viver dentro de uma página de verdade deste site, e a própria página tinha que ser a prova. Este post é essa página.
Este site é Next.js 16 com React 19. O painel abaixo não é. Ele é um app Angular 22, com repositório próprio, build e deploy próprios, carregado em tempo de execução a partir de outro domínio. Tirando uma URL, nada no build deste site sabe que ele existe.
Carregando o micro frontend Angular…
Nenhum evento do micro frontend ainda.
Experimente: troque o tema ou o idioma do site, e o painel acompanha. Clique no botão dele, e a linha logo abaixo, que pertence ao site e não ao Angular, registra o evento. Tudo no painel é lido em tempo de execução: a versão do Angular, o domínio de onde ele veio, o commit em que foi buildado e quanto custou para baixar.
Três formas de compor um frontend
Não existe um padrão único de micro frontends. Existem três comuns, e elas resolvem problemas diferentes.
- Divisão por rota. Cada área do produto é um app separado, e um proxy ou o edge manda cada caminho para o app certo. No Next.js isso se chama multi-zones. É simples de operar; o custo é um carregamento completo de página toda vez que você passa de um app para outro.
- Module Federation. O hospedeiro carrega módulos de outros deploys em tempo de execução e compartilha dependências com eles. No mundo Angular, o Native Federation é a opção de fato. Brilha quando todas as partes usam o mesmo framework.
- Web Components. O micro frontend é empacotado como custom element, e o hospedeiro só precisa renderizar uma tag. É a única interface que React e Angular entendem de forma nativa.
Aqui o hospedeiro é React e o micro frontend é Angular, então é composição entre frameworks. É nesse caso que Web Components são a resposta usual, e é exatamente para isso que o Angular Elements existe.
O contrato
O jeito mais útil que eu encontrei de pensar num micro frontend é como um contrato: tudo o que um lado sabe sobre o outro. Aqui, são três coisas.
**Entradas são locale e theme.** Este site as define como propriedades do elemento; uma página HTML simples pode defini-las como atributos, e o Angular Elements mapeia os dois. O painel tem o próprio dicionário para os cinco idiomas que este site suporta e as próprias cores para cada tema. Nada é herdado do CSS do site.
Saída é um evento DOM. O painel não sabe que o React existe. Ele dispara um CustomEvent, e o site escuta:
this.host.nativeElement.dispatchEvent(
new CustomEvent("mfe:ping", {
detail: { angularVersion: this.angularVersion, at: new Date().toISOString() },
bubbles: true,
composed: true,
}),
)Isolamento é Shadow DOM. O CSS do painel não vaza para a página, e o Tailwind do site não vaza para o painel.
Do lado do Angular, o micro frontend inteiro se resume a registrar uma tag:
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 }))
}Do lado do site, carregar significa importar uma URL em que o bundler não pode mexer e depois esperar a tag ser 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)
})O que custou
Estes números foram medidos no arquivo publicado, não estimados:
- O painel é entregue num único arquivo de 119 KB, ou 42 KB comprimido. Para um painel com um título, uma lista curta e um botão, esse é o preço de levar um segundo framework para a página.
- Na minha conexão, a mediana de cinco cargas frias foi de 44 ms para baixar o arquivo e 50 ms até o elemento ficar pronto para uso.
- Se o arquivo não carrega, o site mostra um aviso curto no lugar do painel em cerca de um terço de segundo, e o resto do post continua funcionando. Para o caso em que o servidor nunca responde, existe um timeout de 8 segundos.
O arquivo só é pedido quando o painel se aproxima da tela, então quem nunca rola até aqui nunca paga pelo Angular.
O que os tutoriais pulam
A maior parte do trabalho não foi o componente. Foram as bordas.
- Confiar em código de outro domínio. O site agora executa JavaScript que ele não buildou. Os dados do post só aceitam um micro frontend vindo de uma origem na lista de permitidas, via HTTPS; qualquer outra coisa é descartada antes de a página renderizar.
- Um arquivo, de propósito. O site importa uma única URL. Se um dia o build do Angular se dividir em chunks, esse contrato quebra em silêncio, então o build falha alto em vez disso.
- Registrar duas vezes. Eu esperava que sair da página e voltar rodasse o módulo de novo e lançasse erro na segunda definição da tag. Não acontece: o navegador avalia um módulo uma vez por URL. O que lança erro é o mesmo arquivo carregado de duas URLs, e ali uma checagem simples também não bastava, porque as duas avaliações passam por ela antes de qualquer uma registrar. Então a checagem roda de novo depois que o Angular sobe.
- Medir entre origens. Sem o header
Timing-Allow-Origin, o navegador informa 0 bytes para arquivos de outros domínios, e o painel não conseguiria distinguir o próprio peso de uma cópia em cache. Uma revalidação 304 tem a mesma armadilha: só os headers trafegam, então o painel diz cache em vez de afirmar que o módulo pesa 0,3 KB. - Deploy independente. O site carrega o arquivo em tempo de execução, então um deploy novo do micro frontend chega a este post sem rebuild do site. Testei publicando uma mudança visível só no painel. O cache curto (60 segundos, mais até 5 minutos servindo a versão anterior enquanto revalida) só limita por quanto tempo quem volta pode ver a antiga.
Quando não vale a pena
Para este site, micro frontend é exagero, e esse era o ponto: eu queria saber o custo antes de precisar pagar por ele. Um segundo framework, um segundo pipeline, um contrato para manter em sincronia e código de outro domínio rodando na página. Com um time só e uma base de código só, um monólito bem organizado entrega quase todos os benefícios por uma fração disso.
Começa a valer quando o problema é gente, não código: vários times, cada um com o próprio ritmo de release, pisando uns nos outros no mesmo frontend. Este site não tem esse problema. Agora eu sei quanto custa resolver.
O código do micro frontend é público: github.com/ViniAguiar1/mfe-angular-inspector.
