Next.js: Modelo Mental dos Server Components
Última atualização: 2026-08-26
O modelo mental RSC é a mudança de paradigma mais importante no Next.js 16 — somente ao entendê-lo você poderá realmente compreender a filosofia de design por trás do App Router.
1. O Que Você Vai Aprender
- A Definição de RSC e Como Funciona o "Zero-Client JS"
- A Fronteira de Renderização Entre o Server Component e o Client Component
- A Diretiva
'use client'e as Regras de Penetração da Fronteira do Cliente - Regras para componentes aninhados: Servidor → Cliente é permitido; Cliente → Servidor não é permitido
- Restrições de Props Serializáveis e o Formato React Flight Payload
2. Uma História Real de uma Desenvolvedora Full-Stack
(1) Ponto de Dor: O Tamanho do Bundle Está Fora de Controle
Alice é uma desenvolvedora full-stack na equipe TaskFlow. A página Dashboard que ela construiu inclui um componente de tabela de dados que referencia três bibliotecas — date-fns, recharts e lodash — simplesmente para formatar datas e desenhar alguns gráficos de barras. O bundle JS inicial da página atingiu 480 KB, e sua pontuação no Lighthouse Performance caiu para 52. Para piorar, esta tabela é meramente um componente renderizado no servidor, puramente apresentacional — os usuários não precisam interagir com ela — mas o código dessas bibliotecas ainda é baixado para o cliente.
(2) Solução com RSC
O RSC garante que os componentes do lado do servidor executem apenas no servidor, produzindo HTML puro e dados serializados, com os bundles JavaScript completamente excluídos.
// app/dashboard/page.tsx — Server Component (Zero-Client JS)
import { getSalesData } from '@/lib/db'
import { formatDistanceToNow } from 'date-fns'
export default async function DashboardPage() {
const sales = await getSalesData() // Acesso Direto ao Banco de Dados
return (
<div>
<h1>Dashboard — {sales.length} registros</h1>
<SalesTable data={sales} />
</div>
)
}
async function SalesTable({ data }: { data: Sale[] }) {
return (
<table>
{data.map(row => (
<tr key={row.id}>
<td>{formatDistanceToNow(row.createdAt)}</td>
<td>{row.amount}</td>
</tr>
))}
</table>
)
}
(3) Resultados
| Dimensão | Client Component Puro | RSC |
|---|---|---|
| Tamanho do Bundle | 480 KB (inclui date-fns + ReCharts + Lodash) | 0 KB (bibliotecas do lado do servidor não são baixadas) |
| Acesso ao Banco de Dados | Requer Rota de API Intermediária | Acesso Direto (Latência Zero) |
| Renderização Above-the-fold | Requer download + execução de JS | HTML Instantâneo |
| SEO | Depende de SSR / renderização no cliente | Suporte nativo |
| Pontuação Lighthouse | 52 | 96 |
3. Definição Básica do RSC
RSC (React Server Component) é um novo tipo de componente introduzido no React 19. Ele executa apenas no servidor e nunca é enviado para o navegador do cliente. O código RSC (incluindo bibliotecas dependentes) não aparece no bundle JS, então você pode usar com segurança bibliotecas grandes, acessar bancos de dados diretamente e ler o sistema de arquivos.
graph TB
subgraph "Lado do Servidor (Server)"
A[Componentes RSC] --> B[Banco de Dados/Sistema de Arquivos/API]
A --> C[Serializar para RSC Payload<br/>Protocolo React Flight]
end
subgraph "Cliente (Navegador)"
D[RSC Payload] --> E[Client Component<br/>Preserva a lógica de interação]
D --> F[Renderização HTML Pura<br/>Zero Sobrecarga de JS]
end
C --> D
style A fill:#d4edda
style D fill:#cce5ff
| Característica | Server Component | Client Component |
|---|---|---|
| Ambiente de Execução | Lado do servidor (Node.js) | Navegador |
| Bundle JS | ❌ Não incluído | ✅ Incluído |
| Banco de Dados/Sistema de Arquivos | ✅ Acesso Direto | ❌ Não Disponível (Requer API) |
| React Hooks (useState/useEffect) | ❌ Não disponível | ✅ Disponível |
| Manipulação de Eventos (onClick/onSubmit) | ❌ Não disponível | ✅ Disponível |
| Async/Await | ✅ Suporte nativo | ❌ Requer tratamento adicional |
(1) O Significado do "Zero-Client JS"
O princípio central do RSC é: Se um componente não tem lógica interativa, seu código não deve ser enviado ao navegador. Isso significa:
- Quando usadas em RSC, bibliotecas exclusivas do lado do servidor como
date-fns,lodashebcryptnão aumentam o tamanho do bundle. - Consultas ao banco de dados são executadas diretamente dentro do componente, sem passar por uma rota de API
- O RSC produz, em última análise, uma string HTML pura, que o navegador renderiza imediatamente
(2) RSC vs. SSR Tradicional
O SSR tradicional (Pages Router) também renderiza HTML no servidor, mas ainda envia o código JavaScript do componente para o cliente para hidratação. O RSC, por outro lado, é completamente diferente — o código dos componentes do lado do servidor nunca chega ao cliente.
| Dimensão | SSR Tradicional (Pages Router) | RSC (App Router) |
|---|---|---|
| Renderização no servidor | ✅ HTML | ✅ HTML |
| Hidratação no Cliente | ✅ Completa | ❌ Não necessária |
| Código do componente enviado ao cliente | ✅ Todo | ❌ Apenas Client Components |
| Preservação de estado | Requer tratamento cuidadoso | Naturalmente sem estado |
| Momento da Obtenção de Dados | getServerSideProps |
Diretamente no componente com await |
▶ Exemplo: Verificando Diferenças no Tamanho do Bundle (Dificuldade: ⭐⭐)
Saída:
Diagrama: Componentes RSC; Banco de Dados/Sistema de Arquivos/API; Serializar para RSC Payload Protocolo React Flight; RSC Payload; Client Component Preserva a lógica de interação; Renderização HTML Pura Zero Sobrecarga de JS.
// app/bundle-demo/page.tsx — Server Component (Puramente no servidor)
import { format, addDays } from 'date-fns'
export default function BundleDemoPage() {
const today = new Date()
const dates = Array.from({ length: 7 }, (_, i) => {
const d = addDays(today, i)
return { label: format(d, 'EEEE'), date: format(d, 'yyyy-MM-dd') }
})
return (
<div>
<h1>Esta Semana</h1>
<ul>{dates.map(d => <li key={d.date}>{d.label}: {d.date}</li>)}</ul>
</div>
)
}
Saída:
Verificação Pós-Build em .next/static/chunks — Exclui o código do date-fns
Saída:
Após o build, o diretório .next/static/chunks não inclui o código do date-fns — confirmada zero sobrecarga de JS no cliente. A página renderiza: cabeçalho "Esta Semana" + 7 itens de lista (ex.: "Segunda-feira: 2026-07-06", "Terça-feira: 2026-07-07", ...).
4. A Diretiva 'use client' e as Fronteiras do Cliente
'use client' é uma diretiva em nível de módulo que marca um componente em um arquivo como Client Component. Quando funcionalidade interativa (useState, onClick, useEffect) é necessária na árvore de componentes RSC, esta diretiva deve ser adicionada no topo do arquivo.
graph TB
A[Root Layout<br/>Server Component] --> B[NavBar<br/>Server Component]
A --> C[DashboardPage<br/>Server Component]
C --> D[SalesChart<br/>'use client']
C --> E[DataTable<br/>Server Component]
D --> F[Lógica de Interação<br/>useState / useEffect]
style A fill:#d4edda
style B fill:#d4edda
style C fill:#d4edda
style D fill:#cce5ff
style E fill:#d4edda
| Comando | Função | Exemplo |
|---|---|---|
'use client' |
Marca o módulo como Client Component | 'use client'; export default function Btn() { ... } |
'use server' |
Marca a função como Server Action | 'use server'; export async function create() { ... } |
(1) Regras de Penetração da Fronteira
Existem duas regras fundamentais na árvore de componentes RSC:
- ✅ Server components podem importar e renderizar client components
- ❌ Client components não podem importar diretamente server components (porque server components existem apenas no lado do servidor)
// ✅ Correto: Server Component Importa Client Component
// app/page.tsx (Server)
import ClientCounter from './ClientCounter'
export default function Page() {
return <ClientCounter />
}
// app/ClientCounter.tsx (Client)
'use client'
import { useState } from 'react'
export default function ClientCounter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
// ❌ Erro: Client Component Não Pode Importar Diretamente Server Component
// app/ClientList.tsx
'use client'
import ServerItem from './ServerItem' // ❌ Erro de Compilação: Server Component não pode ser importado no lado do cliente
export default function ClientList() {
return <ServerItem /> // Esta linha causará um erro.
}
(2) Como Incorporar um Server Component em um Client Component
Dica: Passe dados via children props — o slot children no client component pode receber o resultado da renderização do server component.
// app/layout.tsx (Server)
import ClientShell from './ClientShell'
import ServerSidebar from './ServerSidebar'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<ClientShell sidebar={<ServerSidebar />}>
{children}
</ClientShell>
)
}
▶ Exemplo: A Maneira Correta de Propagar Props — Children Props (Dificuldade ⭐⭐)
Saída:
Inclui uma barra lateral.
Texto visível: }>
{children}
// app/interleaving/ClientWrapper.tsx
'use client'
import { useState } from 'react'
export default function ClientWrapper({ sidebar, children }: {
sidebar: React.ReactNode
children: React.ReactNode
}) {
const [isOpen, setIsOpen] = useState(true)
return (
<div style={{ display: 'flex' }}>
{isOpen && <aside>{sidebar}</aside>}
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
<main>{children}</main>
</div>
)
}
Saída:
Um componente interativo com gerenciamento de estado.
Texto visível: }
// app/interleaving/page.tsx (Server)
import ClientWrapper from './ClientWrapper'
import { getSidebarData } from '@/lib/db'
export default function InterleavingPage() {
const items = getSidebarData() // Obtendo Dados no Lado do Servidor
return (
<ClientWrapper sidebar={<ServerItemList items={items} />}>
<h1>Conteúdo Principal</h1>
</ClientWrapper>
)
}
async function ServerItemList({ items }: { items: string[] }) {
return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>
}
▶ Exemplo: Restrições de Props Serializáveis (Dificuldade: ⭐⭐⭐)
Saída:
Renderiza uma lista de itens usando .map().
Texto visível: }> | Conteúdo Principal
// app/serializable/page.tsx (Server)
function greet() { return 'hello' } // ❌ Funções não são serializáveis
const date = new Date() // ⚠️ Date Não Permitido como Prop RSC
// app/serializable/ClientComponent.tsx
'use client'
export default function ClientComponent(props: {
fn: () => string // ❌ Funções como props → Erro em Tempo de Execução
date: Date // ⚠️ Date → Convertido para string, fuso horário pode ser perdido
data: { name: string } // ✅ Objetos comuns são permitidos
}) {
return <div>{props.data.name}</div>
}
Saída:
Renderiza a interface do componente ClientComponent.
5. Unindo o React Flight Payload com a Árvore de Componentes
Após a renderização do lado do servidor RSC ser concluída, ela produz um formato de dados especial chamado RSC Payload (Protocolo React Flight), que contém a árvore HTML serializada, referências de componentes e dados de props. Quando o cliente recebe o payload, ele o mescla com o client component local para formar a árvore de componentes final.
(1) Estrutura do RSC Payload
sequenceDiagram
participant Server as Servidor Next.js
participant Client as Navegador
Server->>Server: Executar Árvore de Componentes RSC
Server->>Server: Serializar para React Flight Payload
Server->>Client: Enviar RSC Payload + HTML
Client->>Client: Analisar Flight Payload
Client->>Client: Mesclar Client Component (Hidratar)
Client->>Client: Renderizar a interface final
| Componente | Descrição | Exemplo |
|---|---|---|
| Saída do Server Component | Fragmento HTML serializado | <div><h1>Dashboard</h1></div> |
| Referência do Client Component | ID do Módulo + Props | {id: "./chart.js", props: {data: [...]}} |
| Referência de Dados | Resultados da Consulta ao Banco de Dados | {sales: [{id:1, amount: 100}]} |
| Fluxo (Stream) | Divisão por Suspense Boundary | Envio de Múltiplos Chunks Incrementalmente |
▶ Exemplo: Visualizando o RSC Payload (Dificuldade: ⭐⭐)
Visualize a resposta RSC no painel Network das ferramentas de desenvolvedor do navegador:
# Abra o Painel Network, Atualize a página, Filtre por Fetch/XHR
# Encontre a requisição da página atual, Veja a Resposta
# Content-Type: text/x-component significa RSC Payload
Saída:
# Trecho do RSC Payload (Simplificado):
M1:{"id":"./app/page.tsx","chunks":["app/page-abc123.js"]}
J0:["$","div",null,{"children":["$","h1",null,{"children":"Dashboard"}]}]
S1:"react.suspense"
▶ Exemplo: Como um Client Component Referencia um Server Component (Dificuldade: ⭐⭐⭐)
// app/flight-demo/ServerData.tsx — Componente de dados puramente do lado do servidor
export default async function ServerData() {
const data = await fetch('https://api.example.com/data').then(r => r.json())
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
// app/flight-demo/ClientShell.tsx
'use client'
export default function ClientShell({ dataSlot }: { dataSlot: React.ReactNode }) {
return (
<div className="card">
<h2>Client Shell</h2>
<div className="server-data">{dataSlot}</div>
</div>
)
}
Saída:
Renderiza: Client Shell
Texto visível: Client Shell
// app/flight-demo/page.tsx
import ClientShell from './ClientShell'
import ServerData from './ServerData'
export default function FlightDemoPage() {
return (
<ClientShell dataSlot={<ServerData />}>
)
}
6. Exemplo Completo: Arquitetura da Árvore de Componentes RSC
// app/rsc-architecture/layout.tsx — Layout RSC
import ClientShell from './ClientShell'
import { getUser } from '@/lib/auth'
export default async function RscLayout({ children }: { children: React.ReactNode }) {
const user = await getUser() // ✅ Consultas Diretas ao Banco de Dados
return (
<ClientShell username={user?.name ?? 'Convidado'}>
<nav>
<a href="/">Início</a>
<a href="/dashboard">Dashboard</a>
<a href="/settings">Configurações</a>
</nav>
{children}
</ClientShell>
)
}
// app/rsc-architecture/ClientShell.tsx
'use client'
import { useState } from 'react'
import type { ReactNode } from 'react'
export default function ClientShell({ username, children }: {
username: string
children: ReactNode
}) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<div data-theme={theme}>
<header>
<span>Bem-vindo, {username}</span>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Alternar {theme}
</button>
</header>
{children}
</div>
)
}
// app/rsc-architecture/page.tsx
import { getProjects } from '@/lib/db'
export default async function RscArchitecturePage() {
const projects = await getProjects()
return (
<div>
<h1>Projetos ({projects.length})</h1>
<table>
<thead><tr><th>Nome</th><th>Status</th></tr></thead>
<tbody>
{projects.map(p => (
<tr key={p.id}>
<td>{p.name}</td>
<td><StatusBadge status={p.status} /></td>
</tr>
))}
</tbody>
</table>
</div>
)
}
function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
active: '#4caf50', archived: '#9e9e9e', draft: '#ff9800'
}
return <span style={{ background: colors[status] ?? '#ccc', padding: '2px 8px', borderRadius: 4 }}>{status}</span>
}
❓ Perguntas Frequentes
P: RSC e SSR são a mesma coisa? R: Não. Com SSR tradicional, o código JavaScript ainda é enviado ao cliente após o HTML ser renderizado no servidor (hidratação). Com RSC, o código do Server Component nunca é enviado ao cliente — zero sobrecarga de JavaScript. SSR e RSC podem coexistir (o modo padrão do App Router é uma combinação de RSC e SSR).
P: Todos os componentes em um arquivo 'use client' são client components? R: Sim. 'use client' é uma diretiva em nível de módulo — todos os componentes exportados de um arquivo são client components. Recomenda-se separar componentes interativos em arquivos próprios para reduzir a quantidade de código no lado do cliente.
P: Por que um Client Component não pode importar diretamente um Server Component? R: Porque Server Components só existem no ambiente de execução do lado do servidor. Quando um Client Component executa no navegador, o código do Server Component simplesmente não existe. A abordagem correta é conectar os dois usando a prop
childrenou Server Actions.
P: O que acontece se uma função for passada como prop para um client component? R: Um erro será lançado. O RSC payload é baseado em serialização JSON (protocolo React Flight), e funções não podem ser serializadas. Se você precisar passar um callback, deve usar Server Actions ou o padrão de manipulador de eventos.
P: Como determinar se um componente deve ser Server ou Client? R: A regra prática mais simples: se o componente requer interatividade (useState, useEffect, onClick, APIs do navegador), é um Client; caso contrário, use Server Component por padrão. Uma estratégia comum de otimização é extrair as partes interativas para um pequeno wrapper Client, mantendo o corpo principal como Server Component.
P: Qual é a relação entre o RSC payload e o HTML? R: O Next.js 16 envia tanto o HTML (para Carregamento Instantâneo) quanto o RSC payload (para reconstrução da árvore de componentes). O HTML garante que a primeira tela seja exibida imediatamente, enquanto o RSC payload assume as interações após ser analisado no cliente. Juntos, eles proporcionam "primeira tela instantânea + interatividade completa".
📖 Resumo
- RSC (React Server Component) executa apenas no servidor, com zero sobrecarga de JavaScript no cliente
'use client'Indica a fronteira do Client Component; a lógica de interação deve residir no lado do cliente- Um server component pode importar um client component, mas não o contrário (isso pode ser contornado usando a prop
children) - Restrições de Props Serializáveis: Funções, objetos Date e
undefinednão podem ser passados como props RSC - React Flight Payload é o protocolo de serialização do RSC, que inclui trechos HTML, referências de componentes e dados
- Melhor prática: Use server components sempre que possível, e isole a lógica de interação em pequenos wrappers client
📝 Exercícios
-
Exercício Básico (⭐): Crie um Server Component em
app/page.tsxque chame diretamentefetch('https://api.github.com/repos/vercel/next.js')para obter dados e renderizar a contagem de estrelas. Verifique se o cliente não baixa bibliotecas comonode-fetch. -
Exercício Avançado (⭐⭐): Crie uma página contendo um Client Component (botão contador) e um Server Component (lista de usuários), e passe os dados do servidor para o wrapper cliente usando a prop
children. Verifique a resposta RSC Payload na aba Network do DevTools do navegador. -
Desafio (⭐⭐⭐): Construa uma árvore de componentes de três níveis: Layout (Server) → ClientTabs (Client, contendo
useStatepara gerenciar a aba atual) → ServerTabContent (Server Components passados viachildren), onde o conteúdo de cada aba consiste em uma consulta assíncrona de dados diferente. Garanta que a obtenção de dados de todos os Server Components não aumente o tamanho do bundle do cliente.