React Flight: o protocolo por trás dos Server Components
Entenda o que é o React Flight, por que RSC não envia HTML nem JSON puro, como o payload é estruturado em chunks, e como tudo isso se conecta com streaming, bundler, Client Components e Server Actions.
Quando um Server Component renderiza no servidor, a saída principal não é HTML.
Também não é um JSON qualquer saindo de um endpoint.
O que o React produz ali é um payload próprio, feito para transportar árvore de componentes, referências de módulos, valores especiais de JavaScript e unidades de trabalho que podem chegar progressivamente pela rede.
Esse payload é o React Flight.
Se você usa App Router no Next.js, Server Components, Server Actions ou Suspense no servidor, você já depende dele, mesmo sem olhar diretamente para o formato.
E esse é o ponto: boa parte da sensação de "mágica" em RSC vem de não enxergar o que está atravessando o wire.
Quando o Flight fica claro, várias coisas deixam de parecer arbitrárias:
- por que Server Component não envia HTML como saída principal
- por que
"use client"não significa apenas "rode isso no cliente" - por que o bundler entra na arquitetura
- por que
Suspenseconsegue streamar pedaços da árvore - por que um Client Component aparece no payload como referência, e não como resultado executado
- por que Server Actions reutilizam uma infraestrutura parecida de serialização através da mesma fronteira
Este artigo vai montar o modelo inteiro: do problema que o Flight resolve até o formato em chunks e os pontos que mais confundem.
Antes do formato: qual problema o Flight resolve?
Para entender o Flight, vale começar pela pergunta certa.
A pergunta não é "como o React manda HTML do servidor para o cliente?".
A pergunta correta é:
como o React transfere uma árvore React entre duas runtimes diferentes sem perder o modelo de componentes no meio do caminho?
Essa é a base de RSC.
Quando você trabalha com React Server Components, há duas runtimes envolvidas:
- a runtime do servidor
- a runtime do cliente
Cada uma tem suas próprias capacidades.
No servidor, você pode:
- acessar banco de dados
- ler arquivos do disco
- consultar APIs privadas
- usar segredos e credenciais
- rodar componentes assíncronos que esperam I/O
No cliente, você pode:
- manter estado interativo com
useState - responder a clique, input, hover, scroll
- acessar DOM e APIs do navegador
- atualizar a UI instantaneamente sem roundtrip de rede
Essas duas runtimes não compartilham memória. Elas também não compartilham um único sistema de módulos em tempo de execução. O servidor e o cliente são ambientes diferentes.
Então o React precisa de uma forma de fazer uma coisa muito específica:
- executar a parte "servidor" da árvore no servidor
- preservar referências para a parte "cliente" da árvore
- mandar tudo isso para o cliente em um formato que o React consiga continuar processando
- fazer isso de forma incremental, porque algumas partes podem ficar prontas antes de outras
É exatamente aí que o Flight entra.
Por que HTML não basta
É tentador pensar: "se no fim tudo vira tela, por que não mandar só HTML?"
Porque HTML resolve outra etapa do problema.
HTML é ótimo para pintura inicial. Ele permite que o navegador mostre algo rápido na tela. Mas HTML, sozinho, não é uma boa representação daquilo que o React precisa continuar controlando como árvore React.
HTML não carrega, de forma suficiente:
- a distinção entre o que veio de Server Component e o que é Client Component
- referências para módulos cliente que ainda precisam ser carregados
- placeholders de
Suspenseque serão preenchidos depois - valores especiais que não existem em JSON puro
- a estrutura exata que o cliente React quer consumir para continuar a reconciliação e hidratação
Dito de outro jeito: HTML representa resultado visual. O Flight representa estrutura React transferível entre runtimes.
Em frameworks como Next.js, os dois costumam coexistir:
- HTML para o navegador pintar logo
- Flight para o React retomar o controle da árvore
Eles não competem. Eles cumprem papéis diferentes.
Por que JSON puro também não basta
Se HTML não serve, então bastaria JSON?
Também não.
O problema aqui é mais sutil.
Uma árvore React não é apenas um objeto com texto e números. Ela contém coisas que JSON puro não sabe representar bem, ou simplesmente não representa:
- referências a módulos
undefinedDateBigIntMapSet- símbolos globais
- placeholders de streaming
- metadados internos que o cliente React precisa interpretar
Além disso, o React não quer apenas serializar "dados". Ele quer serializar algo mais rico: uma descrição de UI que mistura dados prontos, elementos React e referências para código que ainda será carregado.
É por isso que o Flight pode ser pensado como:
- um protocolo de serialização próprio
- orientado a árvore React
- com extensões sobre JSON
- preparado para streaming
O que um Server Component produz antes de serializar
Antes de virar Flight, um Server Component faz a mesma coisa que qualquer componente React faz: ele retorna elementos React.
Um exemplo simples:
export default async function Page() {
const post = await db.posts.findFirst({
where: { slug: 'react-flight' },
})
return (
<article>
<h1>{post.title}</h1>
<p>{post.summary}</p>
</article>
)
}Depois da compilação do JSX, isso vira objetos simples. Em termos didáticos, pense em algo nessa linha:
{
type: 'article',
props: {
children: [
{
type: 'h1',
props: { children: 'React Flight' },
},
{
type: 'p',
props: { children: 'O protocolo por trás dos RSCs' },
},
],
},
}Esse detalhe é importante porque ajuda a enxergar que o Flight não está serializando HTML. Ele está serializando a árvore React resultante.
E aqui entra uma distinção central:
- quando o componente é um Server Component, o servidor executa a função e serializa o resultado
- quando o componente é um Client Component, o servidor não serializa o resultado da execução dele no Flight; ele serializa uma referência para esse módulo
Essa distinção é a espinha dorsal do protocolo.
Server Components e Client Components não atravessam a fronteira do mesmo jeito
Esse é um dos pontos mais importantes de todo o modelo.
Considere este exemplo:
// Counter.tsx
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
)
}
// Page.tsx
import { Counter } from './Counter'
export default function Page() {
return (
<div>
<h1>Meu app</h1>
<Counter />
</div>
)
}O que acontece aqui?
O Page é servidor. Então ele executa no servidor.
O Counter é cliente. Então o Flight não carrega no payload o botão já "rodado" com useState, onClick e tudo mais como se aquilo fosse apenas um subtree comum do servidor.
Isso não significa que esse componente seja incapaz de participar do HTML inicial em uma estratégia de SSR. Significa apenas que, no payload Flight, ele cruza a fronteira como referência de módulo, não como saída executada de Server Component.
Em vez disso, o Flight carrega uma referência para o módulo cliente. Em essência, algo como:
- qual módulo precisa ser carregado
- qual export daquele módulo representa o componente
- quais chunks do bundler precisam ser baixados
Ou seja:
- Server Component atravessa a fronteira como resultado já executado
- Client Component atravessa a fronteira como referência para código que o cliente vai carregar e executar
Essa diferença, sozinha, já explica metade do comportamento de RSC.
Então o que "use client" faz de verdade?
Muita explicação sobre RSC diz que "use client" marca um componente que "roda no cliente".
Isso não está completamente errado, mas é superficial demais.
O ponto mais preciso é este:
"use client" marca uma fronteira de módulo.
Ele diz ao toolchain: a partir daqui, esse módulo e o que precisa estar do lado dele pertencem ao grafo cliente.
Na prática, quando um módulo servidor importa algo vindo de um arquivo com "use client", ele não recebe a implementação executável real daquele componente para rodar no servidor como parte do Flight. Ele recebe uma referência especial, que depois será materializada no cliente.
Esse jeito de pensar é muito mais útil do que o mantra "isso roda aqui" ou "isso roda ali".
Porque a questão central em RSC não é só onde um módulo foi escrito para executar. A questão central é:
- quando ele é executado
- em qual runtime ele é materializado
- como essa referência cruza a fronteira
O jeito mais útil de pensar nisso é tratar "use client" e "use server" como portas entre duas runtimes, não como adesivos dizendo "este arquivo é cliente" ou "este arquivo é servidor".
Por que o bundler entra na história
Aqui aparece outra dúvida muito comum.
Se o Flight já serializa a árvore, por que bundler é parte tão importante da arquitetura?
Porque Client Components não são valores simples. Eles são módulos.
E serializar um módulo não significa embutir o código fonte do componente como string dentro do payload. Isso seria ruim por vários motivos:
- repetiria código
- explodiria o tamanho da resposta
- exigiria algo parecido com
eval - criaria uma estratégia péssima de cache
O que o Flight precisa é de uma forma estável de dizer ao cliente:
- "carregue este módulo"
- "use este export"
- "esse módulo vive nestes chunks do build"
É exatamente isso que a integração com bundler fornece.
Então o bundler tem, a grosso modo, três papéis aqui:
- encontrar módulos
"use client"durante build - gerar chunks e identificadores para eles
- ensinar servidor e cliente a serializar e desserializar essas referências
Sem isso, o React saberia que existe um componente cliente, mas não teria uma maneira prática de transformá-lo em algo carregável no navegador real.
O formato de alto nível: payload em chunks
No repositório do React, os arquivos centrais dessa lógica ficam em:
O Flight é um protocolo orientado a chunks.
Em vez de montar um JSON gigante e só então enviar tudo, o servidor vai emitindo unidades menores, cada uma identificada por um id.
Na prática, você pode imaginar o payload como uma sequência progressiva de registros. O cliente não precisa esperar "o arquivo inteiro" para começar a entender o que está chegando. Ele vai lendo chunk por chunk e registrando o que cada id representa.
Em uma forma simplificada, a estrutura visual de uma linha é esta:
<id>:<tag opcional><payload>Onde:
ididentifica o chunktagindica o tipo do chunk em alguns casospayloadcontém o valor serializado
Na implementação real, esses ids costumam aparecer em hexadecimal, e o conjunto de tags possíveis é maior do que os exemplos didáticos mais comuns costumam mostrar. O importante aqui não é decorar todas as letras, e sim entender o papel estrutural delas:
- o
iddá identidade a uma unidade do payload - o
payloadguarda o valor daquela unidade - as referências permitem que uma unidade aponte para outra sem precisar inlinear tudo no mesmo lugar
Exemplo simples:
0:{"name":"Matheus","role":"frontend"}Agora um exemplo com referências entre chunks:
0:{"user":"$1","posts":"$2"}
1:{"name":"Matheus","age":24}
2:[{"title":"React Flight"},{"title":"RSC"}]Aqui, o chunk 0 aponta para 1 e 2 usando o prefixo $.
O detalhe crucial é que essas referências permitem composição. Um chunk pode depender de outro, e o cliente consegue montar o todo progressivamente.
Se você quiser visualizar o processo mentalmente, pense assim:
- o cliente recebe
0e vê que ali existem referências para1e2 - ele ainda pode não ter
1e2disponíveis naquele exato instante - então ele registra que o chunk
0depende deles - quando
1e2chegam, ele preenche essas lacunas
Isso significa também que a ordem de chegada não precisa refletir a ordem lógica da árvore. O cliente pode receber algo que referencia um pedaço ainda pendente e resolver isso depois.
Essa propriedade é essencial para streaming.
Como elementos React aparecem serializados
Elementos React no Flight costumam aparecer como arrays nesta forma:
["$", type, key, props]Cada posição tem um papel:
"$"indica que aquilo representa um elemento Reacttypeé o tipo do elementokeyé akeydo elemento, ounullpropsé o objeto de props
Esse primeiro "$" merece um comentário extra. Ele funciona como um marcador de tipo. A ideia é sinalizar para o decoder: "isto aqui não é apenas um array qualquer vindo do payload; isto aqui deve ser interpretado como um React Element".
Isso importa porque, no wire, muita coisa vira estrutura de dados simples. Sem uma marcação assim, o cliente não teria como diferenciar com segurança "um array comum" de "um elemento React serializado".
O campo type também muda de significado dependendo do caso:
- se for uma string como
"div", estamos falando de um elemento host do DOM - se for uma referência especial, estamos falando de algo que precisará ser resolvido como módulo cliente
Ou seja, até dentro do mesmo formato de elemento React o protocolo continua distinguindo o que já é imediatamente renderizável do que ainda depende de outra etapa.
Pegue este JSX:
<div className="container">
<h1>React Flight</h1>
<p>O protocolo por trás dos RSCs</p>
</div>Didaticamente, algo nessa linha pode aparecer no payload:
0:["$","div",null,{"className":"container","children":[["$","h1",null,{"children":"React Flight"}],["$","p",null,{"children":"O protocolo por trás dos RSCs"}]]}]Perceba o que isso quer dizer:
- o servidor não mandou uma string
<div>...</div> - ele mandou uma descrição estruturada da árvore React
- os filhos continuam filhos em nível de dados, não apenas texto concatenado
Isso é o que permite ao cliente React continuar o processamento da árvore com muito mais contexto do que teria se recebesse apenas HTML.
O que o cliente faz quando decodifica Flight
Vale tornar essa etapa explícita, porque ela costuma ficar implícita demais nas explicações.
Quando o cliente recebe o payload Flight, ele não "renderiza texto" diretamente. Primeiro ele precisa decodificar aquele payload para voltar a obter valores e elementos React.
Em termos de modelo mental, o processo é aproximadamente este:
- o cliente lê o stream recebido
- o decoder separa os chunks e registra cada um pelo
id - quando encontra referências, ele liga um chunk ao outro
- quando encontra um elemento React serializado, ele reconstrói essa estrutura como elemento React em memória
- quando encontra referência a módulo cliente, ele aciona o carregamento desse módulo
- quando todas as dependências necessárias para um trecho da árvore ficam prontas, aquele trecho pode ser entregue ao React para renderização ou atualização
Repare que aqui existem duas camadas diferentes de trabalho:
- a camada de decodificação do protocolo
- a camada de renderização/reconciliação do React
Primeiro o cliente precisa entender o Flight.
Depois o React usa o resultado dessa decodificação para continuar seu trabalho normal de render e commit.
Essa separação ajuda a evitar outra confusão comum: achar que o Flight substitui o reconciler do React. Não substitui. O Flight resolve a travessia da árvore pelo wire. A reconciliação continua sendo responsabilidade do React depois que essa árvore é reconstruída.
Tipos que JSON puro não cobre
Como vimos, o Flight estende JSON com convenções próprias.
Alguns valores especiais aparecem usando prefixos começando com $.
Exemplos comuns:
$undefined
$Infinity
$-Infinity
$NaN
$-0
$D2026-04-28T00:00:00.000Z
$n99999999999999999
$Smeu.simbolo.globalEm termos de interpretação:
$undefinedrepresentaundefined$D...representaDate$n...representaBigInt$S...representaSymbol.for(...)
Esse último detalhe importa: o Flight consegue serializar símbolos globais registrados com Symbol.for(), mas não símbolos locais criados com Symbol().
Se uma string literal do usuário começar com $, o protocolo precisa escapar isso para não confundir com um marcador especial. Por isso existe o caso de $$..., que significa "isso aqui é string literal começando com $, não um token do protocolo".
Além disso, algumas estruturas saem em chunks separados, como Map e Set.
Exemplo didático:
1:[["a",1],["b",2]]
0:{"meuMap":"$Q1"}Nesse caso:
- o chunk
1contém a estrutura doMap - o chunk
0aponta para ele com um marcador próprio
O mesmo raciocínio vale para Set, typed arrays e outros tipos especiais.
O ponto importante não é decorar todas as letras do protocolo. O ponto importante é entender a ideia: o Flight pega JSON como base, mas amplia esse espaço para conseguir transportar o tipo de informação que uma árvore React realmente precisa.
Client Components no payload: referências de módulo
Voltemos ao caso do Counter cliente.
Didaticamente, um payload nessa situação pode ter cara de algo assim:
1:I{"id":"./src/Counter.tsx","chunks":["chunk-abc123"],"name":"Counter"}
0:["$","div",null,{"children":[["$","h1",null,{"children":"Meu app"}],["$","$L1",null,{}]]}]O que está acontecendo:
- o chunk
1descreve o módulo cliente - o
Iindica um chunk de import/reference - o chunk
0monta a árvore - dentro da árvore,
"$L1"referencia o componente cliente de maneira lazy
Esse L é importante porque o cliente pode precisar carregar o código do componente antes de renderizá-lo de fato.
É por isso que a história de Client Component no Flight nunca é "o servidor rodou esse componente e mandou o resultado no payload".
A história correta é:
- o servidor encontrou uma fronteira cliente
- o servidor registrou uma referência para esse módulo
- o cliente recebeu essa referência
- o cliente carregou o código correspondente
- só então o componente cliente pôde ser executado naquela runtime
Onde o HTML entra nessa arquitetura
Agora vale amarrar uma confusão muito frequente.
Se o Flight é o payload principal de RSC, então páginas renderizadas com RSC não têm HTML inicial?
Têm, e esse detalhe é importante.
Na prática, frameworks como Next costumam trabalhar com duas saídas relacionadas, mas diferentes:
- uma saída HTML para o navegador pintar logo
- uma saída Flight para o React reconstruir a árvore e continuar a vida interativa do app
Pense assim:
- HTML responde à pergunta "o que eu posso mostrar agora na tela?"
- Flight responde à pergunta "qual árvore React o cliente precisa receber para continuar trabalhando?"
Se você confunde essas duas camadas, fica difícil entender por que existe payload RSC mesmo quando a página aparentemente já chegou pronta no navegador.
O HTML é a pintura inicial.
O Flight é o canal estrutural que o React usa para transportar a árvore entre servidor e cliente.
Streaming: por que o Flight é orientado a linhas e chunks
Aqui está uma das partes mais fortes da arquitetura.
Server Components podem ser assíncronos:
async function SlowComponent() {
const response = await fetch('https://api.exemplo.com/dados')
const data = await response.json()
return <p>{data.title}</p>
}Se o React tivesse que esperar a árvore inteira ficar pronta para só então mandar um blob único ao cliente, perderíamos uma vantagem enorme.
Com Flight, o servidor pode enviar o que já está resolvido e deixar referências pendentes para o que ainda não terminou.
É aqui que Suspense ganha um papel arquitetural forte. Uma boundary de Suspense não é só um fallback visual. Em RSC, ela também funciona como um ponto em que o React pode separar "o que já dá para mandar agora" de "o que ainda depende de trabalho assíncrono".
Exemplo:
export default function Page() {
return (
<div>
<h1>Conteúdo rápido</h1>
<Suspense fallback={<p>Carregando...</p>}>
<SlowComponent />
</Suspense>
</div>
)
}O servidor pode primeiro mandar algo equivalente a:
0:["$","div",null,{"children":[["$","h1",null,{"children":"Conteúdo rápido"}],["$","$S1",null,{}]]}]Aqui, "$S1" representa algo ainda suspenso, ainda não resolvido.
Na prática, isso significa: a shell da árvore já pode seguir viagem, mas aquele ponto específico ainda está aguardando o resultado de um trabalho assíncrono.
Mais tarde, quando a Promise do SlowComponent termina, o servidor manda o chunk complementar:
1:["$","p",null,{"children":"Título do artigo"}]O cliente então resolve a referência pendente e substitui o fallback pelo conteúdo final.
O efeito prático é este:
- o usuário não fica esperando a árvore inteira para ver a primeira pintura útil
- o servidor não precisa segurar toda a resposta por causa de um único trecho lento
- o cliente pode atualizar apenas a parte da árvore que dependia daquele pedaço atrasado
Esse é o coração do streaming em RSC.
E é importante separar duas coisas:
- HTTP chunked transfer é o mecanismo de transporte
- Flight streaming é a lógica do React dizendo quais unidades da árvore já podem ser emitidas e quais ainda estão pendentes
O HTTP só carrega os bytes.
É o Flight que dá significado React a esses bytes.
O protocolo é o mesmo; o modelo do framework pode mudar bastante
Aqui entra um ponto que ajuda muito a não confundir React Flight com o jeito específico que um framework escolhe para usar RSC.
O Flight é o protocolo.
Mas o protocolo, sozinho, não determina toda a arquitetura do framework.
Vale ir com calma aqui, porque esse costuma ser um dos maiores pontos de confusão no assunto.
Quando alguém aprende RSC por um framework específico, é muito fácil misturar três camadas diferentes como se fossem a mesma coisa:
- a capacidade base que o React oferece
- o protocolo Flight usado para transportar a árvore
- a arquitetura que o framework constrói em cima disso
Essas três camadas se encostam o tempo todo, mas não são idênticas.
Se você não separa isso mentalmente, cai em um erro comum: começar a tratar decisões arquiteturais de um framework específico como se fossem propriedades essenciais de React Server Components.
Não é.
Ele não obriga, por exemplo:
- que o servidor seja sempre o dono da árvore inteira
- que toda navegação sempre passe por uma recomposição server-first do mesmo jeito
- que o cache tenha um único formato possível
- que RSC só possa existir acoplado ao roteador principal do framework
O que o React fornece, no nível mais fundamental, é a capacidade de:
- renderizar uma árvore para um Flight stream no servidor
- decodificar esse stream de volta para elementos React
- atravessar a fronteira servidor/cliente usando referências de módulo e valores serializáveis
Se quiser enxergar isso em termos ainda mais mecânicos, a base é algo próximo disto:
- algum código no servidor chama uma primitive que renderiza JSX para um stream Flight
- esse stream atravessa a rede
- algum código no cliente, ou em outra etapa da renderização, decodifica esse stream
- o resultado volta a ser uma árvore de elementos React
- essa árvore é então renderizada, cacheada, recombinada ou reutilizada de acordo com a estratégia do framework
Perceba como, nesse nível, ainda não existe obrigatoriedade sobre roteamento, cache, ownership da árvore ou política de navegação. Essas decisões aparecem depois.
A partir daí, cada framework pode decidir como organizar essa capacidade.
Muita gente aprende RSC quase sempre pelo prisma do Next.js App Router, e isso pode passar a impressão de que aquele modelo específico é "o" modelo de React Server Components.
Não é.
É um modelo possível. Muito importante, muito influente, mas ainda assim um modelo.
O mesmo protocolo, arquiteturas diferentes
Até aqui falamos do Flight no nível do React. A partir daqui entra outra distinção importante: o protocolo é uma coisa; o modelo arquitetural do framework é outra.
O Flight resolve a travessia da árvore entre runtimes.
O framework decide como encaixar isso em:
- roteamento
- cache
- recomposição da UI
- fronteiras entre servidor e cliente
É por isso que dois frameworks podem usar React Server Components e, ainda assim, dar sensações bem diferentes para quem desenvolve.
Um contraste concreto: Next.js e TanStack Start
No App Router do Next.js, o modelo dominante é fortemente server-first.
O fluxo mental costuma ser este:
- uma rota ou segmento é resolvido no servidor
- o servidor recompõe a árvore relevante
- o Flight dessa árvore é emitido
- o cliente recebe esse resultado e continua a parte interativa
Nesse desenho, a árvore principal nasce no servidor, e o Flight aparece como a espinha dorsal dessa composição.
Já em propostas mais isomórficas, como a do TanStack Start, a ênfase muda.
Vale situar rapidamente o exemplo: o TanStack Start é um framework que também trabalha com RSC, mas tenta encaixar esse modelo em uma filosofia menos centrada em uma árvore server-owned e mais aberta a composição, fetch e cache em camadas que já existem na aplicação.
A ideia ali não é redefinir o que RSC ou Flight são, e sim tratar o Flight de forma mais explícita como um recurso que pode ser:
- produzido no servidor
- obtido sob demanda
- decodificado em outro ponto da renderização
- cacheado em camadas já familiares, como router, query ou HTTP
Na prática, isso muda o modelo mental.
Em vez de pensar apenas "o servidor recompõe a árvore e me entrega o próximo estado dela", você também pode pensar algo como:
- "este trecho de UI servidor pode ser buscado como um recurso assíncrono"
- "eu posso armazenar esse resultado em cache"
- "eu posso decidir onde encaixar essa subárvore dentro de outra composição"
O ponto importante é que o protocolo não mudou.
O servidor continua produzindo Flight streams. O cliente continua decodificando Flight streams. Client Components continuam atravessando a fronteira como referências de módulo.
O que muda é o grau de centralidade do servidor e o lugar onde o framework encaixa o payload no ciclo de vida da aplicação.
O que realmente varia entre frameworks
Quando você compara frameworks diferentes, a diferença real costuma aparecer nestas perguntas:
- quem dispara a obtenção do payload Flight?
- esse payload nasce naturalmente do fluxo de rota ou pode ser pedido de forma mais granular?
- quem possui a composição final da UI: o servidor, o cliente, ou ambos em graus diferentes?
- em que camadas esse payload pode ser cacheado?
- quanta convenção o framework impõe para recompor a fronteira entre conteúdo servidor e interatividade cliente?
Essas perguntas mudam bastante a ergonomia do framework, mas não mudam a definição do Flight.
Por isso vale a pena separar bem as camadas:
- o React oferece primitives para renderizar e decodificar Flight
- o Flight define como a árvore atravessa a fronteira
- o framework escolhe como usar isso em SSR, navegação, cache e composição
Quando essa separação fica clara, você deixa de achar que cada framework está "inventando outro RSC".
Na maior parte do tempo, ele só está organizando o mesmo protocolo de maneiras diferentes.
O cliente precisa conhecer chunks fora de ordem
Esse detalhe merece destaque porque ele aparece pouco nas explicações curtas.
Se o protocolo é realmente orientado a streaming, o cliente não pode presumir que tudo chegará na ordem "perfeita".
Então o cliente Flight mantém estruturas internas para:
- registrar chunks já recebidos
- registrar referências ainda não resolvidas
- acordar partes da árvore quando o chunk faltante chega
Isso é o que permite algo como:
- receber uma árvore que aponta para um pedaço ainda inexistente localmente
- renderizar fallback de
Suspense - mais tarde receber o chunk faltante
- encaixar esse chunk no lugar certo
Sem isso, streaming de RSC seria inviável.
Server Actions: a mesma infraestrutura de serialização no caminho inverso
Até aqui, falamos do fluxo servidor -> cliente.
Mas quando você usa Server Actions, também existe um caminho cliente -> servidor que precisa serializar informação.
Exemplo:
// actions.ts
'use server'
export async function salvarPost(titulo, conteudo) {
await db.insert({ titulo, conteudo })
}
// Editor.tsx
'use client'
import { salvarPost } from './actions'
export function Editor() {
async function handleSubmit() {
await salvarPost('React Flight', 'Conteúdo do artigo...')
}
return <button onClick={handleSubmit}>Salvar</button>
}Quando handleSubmit chama salvarPost, o cliente não tem a implementação da action localmente. O que ele tem é uma referência para algo que precisa ser executado no servidor.
Esse detalhe é importante: o cliente não recebe a função do servidor como JavaScript executável para rodar localmente. O que ele possui é uma referência especial, normalmente associada pelo build a um identificador que permite ao servidor descobrir qual action deve ser chamada.
Então o React:
- serializa os argumentos
- envia essa chamada para o servidor
- o servidor desserializa os valores
- encontra a action correta
- executa a função no servidor
Perceba a assimetria:
- no fluxo RSC tradicional, o servidor envia árvore para o cliente
- no fluxo de Server Action, o cliente envia argumentos para disparar trabalho no servidor
Nos dois casos existe uma fronteira entre runtimes e existe serialização atravessando essa fronteira. O que muda é a direção do tráfego e o tipo de coisa que está sendo transportada.
Ou seja, a mesma ideia de "duas runtimes e uma fronteira entre elas" aparece de novo.
Muda a direção do tráfego, mas o problema estrutural continua sendo parecido: transportar referências e dados de forma que o outro lado consiga reconstruir a chamada com sentido React.
O ponto mais seguro de afirmar aqui é: Server Actions reutilizam a mesma família de serialização e de fronteira conceitual do Flight, mesmo que os detalhes exatos do transporte não devam ser tratados como uma API estável de baixo nível para uso manual.
O que costuma confundir em React Flight
1. "O payload do Flight é HTML"
Não.
HTML pode existir junto, como saída complementar para SSR. Mas o Flight, em si, é outro formato e serve a outro propósito.
2. "Client Component não aparece no payload"
Aparece, sim.
O que não aparece é o resultado executado dele do mesmo jeito que acontece com Server Components. O que aparece é a referência ao módulo cliente.
3. "use client significa executa só no cliente"
Não exatamente.
"use client" marca fronteira de módulo para o toolchain. Ele define como aquele módulo cruza a arquitetura RSC. Essa formulação é melhor do que tratar a diretiva como um simples rótulo geográfico.
4. "O Flight é uma API pública para eu escrever payloads manualmente"
Também não.
O Flight é um detalhe interno importante de entender, mas não é um formato pensado para autoria manual. Você não deveria escrever chunks na mão no uso normal.
O valor de estudá-lo está no modelo mental, não em produzir payload manualmente.
Além disso, o formato concreto não deve ser tratado como um contrato público rígido entre versões. Ele é uma peça interna importante da arquitetura do React, mas quem deve lidar com os detalhes operacionais dele no dia a dia são o framework, o bundler e os bindings correspondentes.
5. "Qualquer valor JavaScript pode atravessar RSC"
Não.
Existe um conjunto de tipos suportados pelo protocolo, e esse conjunto é muito mais restrito do que "qualquer coisa serializável em JavaScript".
Isso vale especialmente para valores com comportamento, identidade local ou dependência de contexto de execução.
Por exemplo:
- funções arbitrárias não atravessam livremente
- instâncias de classes customizadas não devem ser tratadas como genericamente suportadas só porque "viram objeto"
- valores dependentes de recursos locais de uma runtime podem simplesmente não fazer sentido do outro lado
Funções arbitrárias, por exemplo, não atravessam a fronteira livremente. Referências especiais de Server Actions são uma exceção controlada pela arquitetura, não uma permissão genérica para mandar funções como prop.
6. "Flight e JSON são a mesma coisa"
Também não.
O Flight usa JSON como base em muitos casos, mas estende a semântica com tokens, referências, tags de chunk e tipos especiais.
Um detalhe importante: Flight é protocolo, não necessariamente HTML, nem necessariamente JavaScript para sempre
No fundo, o Flight é um protocolo.
Ou seja, em teoria, nada obriga a ideia a existir só em um backend JavaScript gerando HTML de forma tradicional.
Se outro ambiente conseguir:
- emitir o formato correto
- representar referências de módulo do jeito esperado pelo cliente
- respeitar as regras de serialização e resolução
então ele pode participar desse modelo.
Na prática, hoje o ecossistema real ainda depende fortemente das implementações oficiais do React e das integrações com bundlers JavaScript. Então esse potencial "agnóstico de linguagem" existe mais como consequência conceitual do protocolo do que como algo completamente estável e padronizado para qualquer stack.
Mas a direção da ideia importa: o núcleo do problema não é HTML. O núcleo do problema é transportar árvore React, referências e unidades de trabalho entre runtimes.
Fechando o modelo mental
Se eu tivesse que resumir React Flight em poucas ideias fundamentais, seriam estas:
- React Flight é o protocolo de serialização usado pelo React para transportar árvore React entre servidor e cliente em RSC
- ele não envia apenas dados comuns; ele envia elementos React, referências de módulos cliente, placeholders de streaming e tipos especiais que JSON puro não cobre
- Server Components atravessam a fronteira como resultado da execução
- Client Components atravessam a fronteira como referências para módulos que o cliente vai carregar
- o bundler participa porque serializar um componente cliente significa serializar uma referência carregável para código, não embutir código fonte bruto no payload
- o formato em chunks permite streaming progressivo, com resolução de referências fora de ordem
- HTML e Flight coexistem, mas cumprem papéis diferentes
- Server Actions reutilizam a mesma ideia de fronteira e uma infraestrutura parecida de serialização, agora no caminho cliente -> servidor
Quando esse modelo fica firme, RSC deixa de parecer uma caixa-preta.
Você passa a enxergar que React Server Components não são "componentes mágicos do servidor". São componentes cujo resultado entra em um protocolo específico, o Flight, para que outra runtime consiga continuar a história dali.
E quando fica claro o que esse protocolo precisa carregar, quase todas as restrições e capacidades de RSC começam a fazer sentido.
Se você quiser complementar esse modelo com a parte de renderização no cliente depois que a árvore chega, vale ler Por que o React re-renderiza: o modelo mental completo.