vitral
| Install | |
|---|---|
npm install vitral |
|
| Latest Version: | 0.7.2 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 27, 2026 |
| Links: | GitHub · npm |
Documentação · Começar · Por que o vitral · Benchmark · English
Junte micro-frontends de qualquer framework numa página só, sem configuração de build no host.
Cada equipe publica o próprio pedaço da página (um remote) com vite build. O host recebe a URL de cada remote e monta tudo no navegador.
Por que usar:
- Zero configuração no host. Sem plugin de build: só
configure({ remotes })com a URL de cada remote. - Qualquer framework. React, Next, Vue, Nuxt, Angular, Svelte, Solid, Preact, Lit e JS puro na mesma página. Sem combinar versão de React entre equipes.
- Deploy independente. Cada equipe publica quando quiser. O host vê a versão nova sem novo build.
- CSS e erros isolados. O CSS de um remote não muda o resto da página. Um remote fora do ar mostra o próprio erro, e o resto continua funcionando.
- Seguro. O JS só roda se vier de uma origem permitida e se o hash SHA-384 bater.
- Leve e rápido. 3,4 kB de host. Primeira visita 57% mais rápida que o Module Federation e troca de área 4,6 vezes mais rápida que o Next.js Multi-Zones.
npm install vitral
Comparação
| Module Federation | single-spa | Next.js Multi-Zones | vitral | |
|---|---|---|---|---|
| Configuração no host | Plugin no build, combinado com cada remote | Root config e import map | Um app Next por zona e rewrites | Nenhuma. Só configure() |
| Frameworks | Combinar versões em shared |
Um adaptador e um ciclo de vida por framework | Só Next | Qualquer um, sem combinar versão |
| Divide a página em | Pedaços | Apps por rota | Rotas (cada zona é uma página) | Pedaços ou rotas |
| Trocar de área | Sem recarregar | Sem recarregar | Recarrega a página e baixa Next e React de novo | Sem recarregar |
| Isolamento de CSS | Nenhum | Nenhum | Por página | Escopo automático ou Shadow DOM |
| Remote fora do ar | Erro sem tratamento derruba a página | A tela de erro é com você | A zona inteira fica fora | Só aquele remote mostra erro, com retry |
| Código de host (brotli) | 19,4 kB | 5,8 kB | Runtime do Next em cada zona | 3,4 kB |
Medido no benchmark público (Chromium, rede Slow 4G, mediana de 7 visitas):
- 57% menos tempo na primeira visita que o Module Federation com React compartilhado (1157 ms contra 2673 ms, 1 remote).
- 4,6 vezes mais rápido para trocar de área que o Next.js Multi-Zones (184 ms contra 850 ms), com 63% menos bytes para abrir duas áreas.
- 10 frameworks cobertos por testes de ponta a ponta no CI, com host e remote de frameworks diferentes, em Chromium, Firefox, WebKit e mobile.
O método e as versões usadas estão na página do benchmark.
Começo rápido
Para gerar host e remote prontos: npm create vitral@latest.
1. No remote, exporte o componente e use o plugin:
// src/App.jsx (ou .vue, .svelte, .tsx…)
export default function Carrinho({ userId }) { … }
// vite.config.js
import react from '@vitejs/plugin-react'
import vitral from 'vitral/vite'
export default { plugins: [react(), vitral()] }
O plugin descobre o framework e embrulha o export default com o adaptador certo. A entrada padrão é src/remote.* ou src/App.*.
vite build gera o JS, o CSS e um vitral.json. Publique a pasta dist em qualquer CDN com CORS.
2. No host, registre a URL e use o componente:
import { configure } from 'vitral'
import { Remote } from 'vitral/react'
configure({
remotes: { carrinho: 'https://cdn.empresa.com/carrinho/vitral.json' },
})
export function App() {
return (
<Remote
name="carrinho"
props={{ userId: 42 }}
fallback={<p>Carregando…</p>}
error={(erro, tentarDeNovo) => <button onClick={tentarDeNovo}>Tentar de novo</button>}
/>
)
}
Pronto. Mudar props atualiza o remote sem remontar.
Sem build no host, numa página HTML qualquer:
<script type="module" src="https://cdn.jsdelivr.net/npm/vitral/dist/auto.js"></script>
<vitral-remote src="https://cdn.empresa.com/carrinho/vitral.json"></vitral-remote>
Todos os componentes de host aceitam src no lugar de name.
Rodar local
Cada remote roda no próprio vite dev, numa porta. O host aponta para essas portas:
cd carrinho && npm run dev # vite --port 5174
cd busca && npm run dev # vite --port 5175
cd loja && npm run dev # vite --port 3000
// loja/public/remotes.json
{
"carrinho": "http://localhost:5174/vitral.json",
"busca": "http://localhost:5175/vitral.json"
}
// loja/src/main.jsx
configure({ remotes: '/remotes.json' })
Editar o remote atualiza o host sem recarregar a página (HMR). Para rodar só um remote local e pegar os outros de homologação, abra o host com ?vitral=carrinho@http://localhost:5174.
Docker e Kubernetes
Cada remote vira uma imagem nginx com o dist. O host lê a lista de remotes de uma variável de ambiente quando o contêiner sobe, então a mesma imagem serve em qualquer ambiente.
# compose.yaml
services:
loja: # host: http://localhost:8080
build: ./loja
ports: ["8080:80"]
environment:
VITRAL_REMOTES: '{"carrinho":"/mf/carrinho/vitral.json","busca":"/mf/busca/vitral.json"}'
carrinho: # remote React
build: ./carrinho
busca: # remote Vue
build: ./busca
O nginx do host repassa /mf/carrinho/ para o contêiner carrinho. Tudo fica na mesma origem: sem CORS e com uma porta só. Parar o contêiner carrinho mostra o erro só no lugar do carrinho.
No Kubernetes, as mesmas imagens funcionam com um Service por remote; o Ingress pode fazer o papel do nginx do host. Se os remotes ficam num CDN, troque VITRAL_REMOTES pelas URLs do CDN.
Exemplo completo, com Dockerfiles e configuração do nginx: examples/docker.
Frameworks
| Framework | Remote | Host | Import |
|---|---|---|---|
| React 18/19 | reactRemote(App) |
<Remote> |
vitral/react |
| Next.js (App e Pages Router) | reactRemote(App) |
<Remote> |
vitral/react |
| Vue 3 | vueRemote(App, { setup }) |
<VitralRemote> |
vitral/vue |
| Nuxt 3/4 | vueRemote(App, { setup }) |
<VitralRemote> |
vitral/vue |
| Angular 17+ | angularRemote(Component, { providers }) |
<vitral-remote> + provideVitral() |
vitral/angular |
| Svelte 5 | svelteRemote(App) |
use:remote |
vitral/svelte |
| Solid | solidRemote(App) |
<Remote> |
vitral/solid |
| Preact | preactRemote(App) |
<Remote> |
vitral/preact |
| Lit e Web Components | elementRemote('meu-widget') |
<vitral-remote> |
vitral/element |
| JS e TypeScript puro | component({ render }) |
<vitral-remote> |
vitral |
No Next e no Nuxt o componente renderiza o fallback no servidor e monta o remote no cliente.
Host em Vue
<script setup>
import { VitralRemote } from 'vitral/vue'
</script>
<template>
<VitralRemote name="carrinho" :props="{ userId: 42 }">
<template #fallback>Carregando…</template>
<template #error="{ retry }"><button @click="retry">Tentar de novo</button></template>
</VitralRemote>
</template>
Host em qualquer outro lugar
<vitral-remote> funciona em HTML puro, Angular, Svelte, Astro ou HTML gerado no servidor:
<script type="module">
import { configure } from 'vitral'
import { defineVitralElement } from 'vitral/element'
configure({ remotes: { carrinho: 'https://cdn.empresa.com/carrinho/vitral.json' } })
defineVitralElement()
</script>
<vitral-remote name="carrinho" props='{"userId": 42}'>
<p slot="fallback">Carregando…</p>
<p slot="error">Indisponível</p>
</vitral-remote>
Remote sem framework
import { component, html } from 'vitral'
export default component<{ nome: string }>({
state: { cliques: 0 },
render: ({ nome }, state) => html`
<p>Olá, ${nome}</p>
<button onclick=${() => state.cliques++}>cliques: ${state.cliques}</button>
`,
})
O que entra em ${} é escapado. Mudar state ou as props atualiza a tela.
Desenvolvimento
- HMR: com o remote em
vite dev, aponte o host parahttp://localhost:5174/vitral.json. Editar o remote atualiza o host sem recarregar a página. - Overrides:
http://localhost:3000/?vitral=carrinho@http://localhost:5174troca a URL de um remote sem mexer no host. Só vale emlocalhostou comconfigure({ devOverrides: true }). Para guardar no navegador, usedevtools()devitral/devtools. - Tipos das props: o build publica
vitral.d.ts. No host,npx vitral typesgeravitral-env.d.ts, e uma prop errada passa a não compilar.
Comunicação entre remotes
import { events } from 'vitral'
events.emit('carrinho:total', 120)
const parar = events.on('carrinho:total', (total) => console.log(total))
events.last('carrinho:total') // 120
Todos os remotes usam o mesmo barramento. O que um remote assina é cancelado quando ele desmonta.
Para estado comum (usuário, tema, carrinho), use o contexto compartilhado:
import { provide, shared } from 'vitral'
provide('usuario', { id: 42 }) // host: cria e é o dono
provide('tema', 'claro', { writable: false }) // só o dono altera
shared('tema').subscribe((tema) => console.log(tema))
const nome = useShared('usuario', (u) => u?.nome) // React, Preact, Vue e Solid
Angular usa injectShared, Svelte usa $tema e Lit usa SharedController. Detalhes em Contexto compartilhado.
Para mudar de página de dentro de um remote, use navigate('/pedidos/42'). Todos os roteadores da página recebem a mudança.
CSS e Tailwind
Use Tailwind ou qualquer CSS no remote. O plugin prefixa o CSS para valer só dentro do remote, então o CSS de um remote não muda o host nem os outros remotes.
Para isolar por completo, use Shadow DOM:
<Remote name="carrinho" isolate="shadow" />
Com Shadow DOM, as regras @property e @font-face sobem para o documento, então sombras, rings, gradientes e fontes do Tailwind v4 funcionam. O plugin já faz essa separação no build (campo shadow do vitral.json): a página só adota o CSS pronto. Manifestos antigos continuam funcionando, com a separação feita no navegador.
O remote também pode usar o tema do host:
vitral({ theme: 'host' }) // var(--x) usa o valor do host, se existir
configure({ styles: { inherit: ['/assets/base.css'] } }) // CSS base do host dentro do Shadow DOM
Tema com Tailwind v4, dark mode, CSS legado e Tailwind v3 no host: Estilos.
Configuração
configure({
remotes: '/remotes.json', // ou { nome: url }
allowedOrigins: ['https://cdn.empresa.com'], // padrão: a página e as origens registradas
timeout: 15000, // ms por download
retries: 1, // novas tentativas em erro de rede
isolate: 'none', // ou 'shadow'
})
Cada remote também aceita um objeto:
configure({
remotes: {
carrinho: { url: 'https://cdn.empresa.com/carrinho/vitral.json', integrity: 'sha384-…' },
menu: { module: () => import('./menu') }, // remote local, sem rede
},
})
Erros
Todo erro é um RemoteError com code, o nome do remote e docs, o link da página do erro com causas e correção. Um remote que falha mostra o próprio erro e não derruba a página. retry monta só aquele remote de novo.
code |
Quando |
|---|---|
NOT_REGISTERED |
Nome não registrado em configure |
ORIGIN_BLOCKED |
URL fora de allowedOrigins |
TIMEOUT |
Não respondeu dentro de timeout |
INTEGRITY |
O hash do JS não confere |
INVALID_MODULE |
O arquivo não exporta mount() |
LOAD_FAILED |
Erro de rede ou HTTP |
MOUNT_FAILED |
O remote lançou erro ao montar |
RENDER_FAILED |
O remote quebrou depois de montado |
Performance
Remote com import() também gera dist/split/: o navegador baixa só o que o remote usa ao abrir, e o resto chega no import(), conferido pelo mesmo hash. Hosts antigos continuam usando o arquivo único.
No CDN, sirva vitral.json com Cache-Control: no-cache e o resto (nome com hash) com public, max-age=31536000, immutable.
Assim que o manifesto chega, o JS e o CSS do remote baixam juntos. A montagem
espera os dois, para o remote não aparecer sem estilo. configure() abre a
conexão com a origem de cada remote (<link rel="preconnect">).
import { preload } from 'vitral'
// Baixa JS e CSS sem montar, por exemplo no hover de um link
link.addEventListener('pointerenter', () => preload('checkout'), { once: true })
Nos componentes de host e no <vitral-remote>:
<!-- monta perto de aparecer na tela; reserve o espaço para a página não pular -->
<vitral-remote name="comentarios" loading="lazy" style="display:block;min-height:400px"></vitral-remote>
<!-- monta com o navegador ocioso, mas já baixa no hover -->
<vitral-remote name="checkout" loading="idle" prefetch="hover"></vitral-remote>
<!-- remote que aparece logo ao abrir a página -->
<vitral-remote name="cabecalho" priority="high"></vitral-remote>
<Remote name="checkout" loading="lazy" prefetch="visible" keepAlive />
| Opção | Valores | O que faz |
|---|---|---|
loading |
lazy, idle |
Monta a 200px de aparecer na tela, ou com o navegador ocioso |
prefetch |
hover, visible, idle |
Chama preload() antes de montar |
priority |
high |
fetchpriority alto no manifesto, no JS e no CSS |
keepAlive (keep-alive) |
true ou o máximo |
Ao sair da página, o remote vai para um contêiner oculto (LRU, padrão 5); na volta só recebe as props novas |
Com keep-alive, o remote pode pausar timers e assinaturas:
mount(el, props, ctx) {
ctx.onDeactivate?.(() => clearInterval(timer))
ctx.onActivate?.(() => (timer = setInterval(tick, 1000)))
}
Métricas
Cada etapa do carregamento vira um performance.measure com o nome
vitral:<remote>:<etapa>. As etapas são manifest, js, css e mount.
Elas aparecem na aba Performance do DevTools. Para mandar ao seu RUM:
configure({
onMetric: ({ name, stage, duration }) => rum.send(`vitral.${stage}`, duration, { remote: name }),
})
O evento vitral-ready (e o ready do componente Vue) traz os tempos em
detail.timings, por exemplo { manifest: 12.3, js: 40.1, css: 8, mount: 2.4 }.
Um remote local (module) só mede css e mount. Erros dentro do
onMetric são ignorados.
Dependências compartilhadas
Para não baixar React ou Vue mais de uma vez, liste-os em dedupe no remote:
vitral({ dedupe: ['react'] })
O remote usa a cópia que já está na página quando a versão serve (padrão: ^ da versão instalada no remote) e baixa a própria só quando não serve. Os componentes de host em React oferecem o React do host sem configuração.
Mais formas de usar
Vários componentes por remote (<Remote name="carrinho/MiniCarrinho" />),
módulos sem UI (loadModule('carrinho/api')), deploy novo sem recarregar a
página (watchUpdates() e refresh('carrinho')) e app shell por rota
(routes() e <vitral-outlet>, em vitral/routes). Veja
Exposes, Atualização sem reload e Rotas.
Segurança
- Só importa JS de origens permitidas, mesmo que o
vitral.jsonaponte para outra. Osrcde um componente de host entra na allowlist; o atributosrcdo<vitral-remote>não entra quando háallowedOrigins. - Com
integrity, nada roda sem o hash conferir. A lib usa o primeiro caminho que o navegador garante: import map comintegrity(Chrome 133+, Safari 18.4+),<link rel="modulepreload" integrity>(Firefox 115+) ou, nos outros, baixa, confere e executa exatamente os bytes conferidos viablob:. Aceitasha256,sha384esha512, e vários hashes separados por espaço (vale o mais forte). O CSS do manifesto também é conferido. - Com CSP, a lib copia o
noncede um<script nonce>da página para o import map. Sem nonce, cai para omodulepreload. - O hash só protege contra CDN comprometido se vier de outro lugar: fixe
integrityno host ou embuta o manifesto ({ url, manifest }). - Sem
evale sem iframes. - Shadow DOM isola o CSS, não o JS. Para código de terceiros em que você não confia, use iframe.
Testes do host
import { mockRemote, failingRemote } from 'vitral/testing'
const carrinho = mockRemote('carrinho', (el, props) => (el.textContent = `itens: ${props.itens}`))
// renderize o host
expect(carrinho.lastProps).toEqual({ itens: 2 })
failingRemote('recomendacoes') // testa a tela de erro
Links
- Exemplos: o mesmo remote em 8 frameworks e 9 hosts.
- Como contribuir
- Licença MIT. Feito por Pacheco Codes.
Related Packages
Universal Microfrontends Framework - 13 frameworks, zero config, Shadow DOM isol...
integrate zikojs elements within other ui framework like vue react solidjs svelt...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.7.2 | >=3.3 | MIT | |
| 0.7.1 | >=3.3 | MIT | |
| 0.7.0 | >=3.3 | MIT | |
| 0.6.0 | >=3.3 | MIT | |
| 0.5.0 | >=3.3 | MIT | |
| 0.4.0 | >=3.3 | MIT | |
| 0.3.0 | >=3.3 | MIT | |
| 0.2.0 | >=3.3 | MIT | |
| 0.1.0 | >=3.3 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |