Next.js: Busca de Dados: fetch & RSC
Última atualização: 2026-08-26
No RSC, o
fetchnão é mais apenas uma busca do navegador — ele estende a camada de cache, permitindo que você controle o ciclo de vida dos dados de forma declarativa.
1. O Que Você Vai Aprender
- Comportamento de cache automático do
fetch(url, options)no Server Component - Três modos de cache:
force-cache,no-store,revalidate next: { tags }erevalidateTag()para revalidação sob demanda- Busca Paralela de Dados com
Promise.all()para Evitar o Efeito Cascata (Waterfall) - Identificação e Otimização de Fluxos em Cascata Serial
2. Uma História Real de um Desenvolvedor Full-Stack
(1) Ponto de Dor: O dashboard leva 8 segundos para carregar
Bob é o Líder Técnico da equipe TaskFlow. A página Dashboard precisa carregar cinco fontes de dados: estatísticas de usuários, número total de projetos, tarefas recentes, logs de atividade e notificações do sistema. O código inicial usava cinco chamadas seriais await fetch(...), cada uma esperando a anterior terminar — resultando em um tempo total de 2,1s + 1,8s + 1,5s + 0,9s + 1,7s = 8 segundos. Os usuários reclamavam que a página "demorava muito para carregar". Para piorar, a API era consultada novamente a cada atualização, fazendo a carga do banco de dados disparar para 5.000 QPS.
(2) A Solução com fetch do Next.js
Use
Promise.all()para requisições paralelas +next: { revalidate: 60 }para cache de 60 segundos.
// app/dashboard/page.tsx
export default async function DashboardPage() {
const [users, projects, tasks, logs, notifs] = await Promise.all([
fetch('https://api.example.com/stats/users', { next: { revalidate: 60 } }),
fetch('https://api.example.com/stats/projects', { next: { revalidate: 60 } }),
fetch('https://api.example.com/stats/tasks', { next: { revalidate: 30 } }),
fetch('https://api.example.com/activity/logs', { cache: 'no-store' }),
fetch('https://api.example.com/notifications', { next: { revalidate: 10 } }),
]).then(responses => Promise.all(responses.map(r => r.json())))
return <DashboardView {...{ users, projects, tasks, logs, notifs }} />
}
(3) Resultados
| Dimensão | Antes da Otimização | Depois da Otimização |
|---|---|---|
| Tempo de carregamento | 8 segundos (serial) | 2,1 segundos (paralelo) |
| QPS do Banco de Dados | 5.000 | 83 (cache de 60s) |
| Reclamações de Usuários | 12 por dia | 0 |
| Linhas de código | 35 linhas (5 fetches separados) | 10 linhas |
3. Os Três Modos de Cache do fetch
O Next.js 16 estende a API Web fetch adicionando três modos de cache. Todos os fetch no RSC usam force-cache (cache automático) por padrão, a menos que outro modo seja explicitamente especificado.
graph LR
A[RSC fetch] --> B{Modo de Cache}
B --> C[force-cache<br/>Valor padrão]
B --> D[no-store<br/>A cada atualização da página]
B --> E[revalidate:N<br/>Janela de Tempo]
C --> F[Cache de Dados<br/>Armazenamento Persistente]
D --> G[Dados em Tempo Real<br/>Sem cache]
E --> H[N segundos em cache<br/>Reobtém após expirar]
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#fff3cd
| Padrão | Sintaxe | Comportamento | Casos de Uso |
|---|---|---|---|
force-cache (padrão) |
fetch(url) ou fetch(url, { cache: 'force-cache' }) |
Obtido apenas durante o build ou na primeira requisição; resultados são cacheados permanentemente | Dados que raramente mudam (documentos, configuração estática) |
no-store |
fetch(url, { cache: 'no-store' }) |
Obtém dados novamente a cada requisição; sem cache | Dados em tempo real (informações do usuário, inventário) |
revalidate:N |
fetch(url, { next: { revalidate: 60 } }) |
Cache por 60 segundos; o processo em segundo plano dispara uma atualização ao expirar | Dados semi-tempo real (notícias, rankings) |
(1) Comportamento Padrão force-cache
Se nenhuma opção for passada, o Next.js automaticamente armazena em cache os resultados do fetch — requisições com a mesma URL e opções são feitas apenas uma vez durante o processo de build.
// app/products/page.tsx — force-cache Padrão
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products').then(r => r.json())
// Obtém uma vez durante o build, Usa o cache depois
return <ProductList data={products} />
}
(2) no-store Dados Dinâmicos
// app/profile/page.tsx — Obtém os dados mais recentes a cada requisição
export default async function ProfilePage() {
const user = await fetch('https://api.example.com/me', {
cache: 'no-store',
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
}).then(r => r.json())
return <ProfileView user={user} />
}
(3) revalidate Janela de Tempo
// app/blog/[slug]/page.tsx — Cache Estilo ISR
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 } // Usa o cache por 1 hora
}).then(r => r.json())
return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}
▶ Exemplo: Comparação dos Três Modos de Cache (Dificuldade: ⭐)
Saída:
Obtém dados e renderiza o resultado.
// app/cache-demo/page.tsx
export default async function CacheDemoPage() {
const staticData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
cache: 'force-cache'
}).then(r => r.json())
const liveData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
cache: 'no-store'
}).then(r => r.json())
return (
<div>
<p>Estático (force-cache): {staticData.datetime}</p>
<p>Ao Vivo (no-store): {liveData.datetime}</p>
</div>
)
}
Saída:
Estático (force-cache): 2026-07-06T10:00:00.000Z ← Sempre o Mesmo
Ao Vivo (no-store): 2026-07-06T10:00:05.123Z ← Muda a cada atualização
Saída:
O navegador renderiza dois timestamps:
Estático (force-cache): 2026-07-06T10:00:00.000Z ← Sempre o mesmo (cacheado no build)
Ao Vivo (no-store): 2026-07-06T10:00:05.123Z ← Muda a cada atualização
4. Revalidação Sob Demanda: tags e revalidateTag
next: { tags: [...] } Etiqueta a requisição fetch, depois use revalidateTag(tag) para atualizar o cache conforme necessário na Server Action ou Route Handler.
sequenceDiagram
participant A as Server Action
participant Cache as Cache de Dados
participant DB as Banco de Dados
A->>DB: Escrever Novos Dados (Criar uma Tarefa)
A->>Cache: revalidateTag('tasks')
Cache->>Cache: Limpar todos os caches que correspondem à tag
Note over Cache: Na próxima vez, o fetch Obtém Novamente
| API | Finalidade | Onde Chamar |
|---|---|---|
next: { tags: ['tasks', 'projects'] } |
Etiquetar o "fetch" | Opções do fetch() |
revalidateTag('tasks') |
Limpar todo cache relacionado por tag | Server Action / Route Handler |
revalidatePath('/dashboard') |
Limpar cache por caminho | Server Action / Route Handler |
▶ Exemplo: Usando tags e revalidateTag (Dificuldade: ⭐⭐)
// app/tasks/data.ts — Funções de Obtenção de Dados
export async function getTasks() {
return fetch('https://api.example.com/tasks', {
next: { tags: ['tasks'] }
}).then(r => r.json())
}
// app/tasks/actions.ts — Server Action Atualiza o cache após escrever
'use server'
import { revalidateTag } from 'next/cache'
export async function createTask(formData: FormData) {
const title = formData.get('title') as string
await fetch('https://api.example.com/tasks', {
method: 'POST',
body: JSON.stringify({ title, status: 'todo' })
})
revalidateTag('tasks') // Limpa todas as entradas em cache para esta tag
}
Saída:
Obtém dados e renderiza o resultado.
▶ Exemplo: Usando revalidatePath para limpar a página inteira (Dificuldade: ⭐⭐)
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function publishArticle() {
await db.article.update({ where: { id: 1 }, data: { published: true } })
revalidatePath('/blog') // Atualiza a página /blog
revalidatePath('/blog/[slug]') // Atualiza todos os detalhes dos artigos
}
Saída:
Renderiza a interface do componente publishArticle.
5. Busca Paralela de Dados e Como Evitar o Efeito Cascata (Waterfall)
O padrão cascata (waterfall) é o principal assassino de performance — cada await espera em sequência pelo anterior terminar. Usar Promise.all() permite que todas as requisições sejam iniciadas simultaneamente.
graph LR
subgraph "Padrão Cascata (Lento)"
A1[fetch A] --> A2[fetch B] --> A3[fetch C]
A1 -.- t1[2s]
A2 -.- t2[+2s = 4s]
A3 -.- t3[+2s = 6s]
end
subgraph "Paralelo (Rápido)"
B1[fetch A] -.- u1[2s]
C1[fetch B] -.- u2[2s]
D1[fetch C] -.- u3[2s]
B1 & C1 & D1 --> M[Promise.all<br/>Tempo Total ~2s]
end
| Padrão | Implementação | Tempo Total (2s cada) | Cenários Aplicáveis |
|---|---|---|---|
| Cascata Serial | await A; await B; await C |
~6s | Requisições dependentes |
| Requisições Paralelas | Promise.all([A, B, C]) |
~2s | Requisições independentes |
| Paralelo em fases | const a = await A; const [b, c] = await Promise.all([B(a.id), C]) |
~4s | Requisições parcialmente dependentes |
▶ Exemplo: Reconhecendo o Padrão Cascata Serial (Dificuldade: ⭐)
// app/waterfall/page.tsx — ❌ Cascata Serial
export default async function WaterfallPage() {
const user = await fetch('https://api.example.com/user').then(r => r.json()) // 1s
const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json()) // Espera terminar + 2s = 3s
const details = await Promise.all(tasks.map(t =>
fetch(`https://api.example.com/tasks/${t.id}/details`).then(r => r.json()) // Espera terminar + 2s = 5s
))
return <div>Total: ~5s</div>
}
Saída:
Obtém dados e renderiza uma lista de itens.
Texto visível: Total: ~5s
▶ Exemplo: Otimização Paralela (Dificuldade: ⭐⭐)
Saída:
A página renderiza conforme descrito acima, com a UI atualizando de acordo com o comportamento descrito.
// app/no-waterfall/page.tsx — ✅ Otimização Paralela
export default async function NoWaterfallPage() {
// Estágio 1: Obtém Usuários e Dados Iniciais Concorrentemente
const [user, initialData] = await Promise.all([
fetch('https://api.example.com/user', { next: { revalidate: 10 } }).then(r => r.json()),
fetch('https://api.example.com/initial', { cache: 'no-store' }).then(r => r.json()),
])
// Estágio 2: Requisição dependente de user.id (Ainda há pequenas cascatas, mas já é o melhor)
const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())
return <div>Total: ~2s (1s + 1s paralelo, depois 1s)</div>
}
Saída:
Obtém dados e renderiza o resultado.
Texto visível: Total: ~2s (1s + 1s paralelo, depois 1s)
▶ Exemplo: Carregamento Lazy com Suspense (Dificuldade: ⭐⭐⭐)
Saída:
A página renderiza conforme descrito acima, com a UI atualizando de acordo com o comportamento descrito.
// app/suspense-demo/page.tsx — Cada área separada usa Suspense
import { Suspense } from 'react'
export default function SuspenseDemoPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Carregando perfil...</div>}>
<ProfileSection />
</Suspense>
<Suspense fallback={<div>Carregando tarefas...</div>}>
<TaskSection />
</Suspense>
</div>
)
}
async function ProfileSection() {
const user = await fetch('https://api.example.com/user', { cache: 'no-store' }).then(r => r.json())
return <div>Bem-vindo, {user.name}</div>
}
async function TaskSection() {
const tasks = await fetch('https://api.example.com/tasks', { next: { revalidate: 30 } }).then(r => r.json())
return <ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
}
Saída:
Renderiza um shell estático imediatamente, com conteúdo dinâmico carregando dentro dos limites do Suspense.
Fallback: Carregando perfil...
Texto visível: Dashboard | Carregando perfil... | }> | Carregando tarefas...
6. Exemplo Completo: Dashboard Otimizado
// app/dashboard-optimized/page.tsx
import { Suspense } from 'react'
import { revalidateTag } from 'next/cache'
// ======== Funções de Dados ========
const API = 'https://jsonplaceholder.typicode.com'
async function getData<T>(endpoint: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${API}${endpoint}`, {
...options,
next: { tags: [endpoint.split('/')[1] ?? 'default'], ...(options as any)?.next },
})
if (!res.ok) throw new Error(`Falha ao buscar ${endpoint}`)
return res.json()
}
// ======== Obtém todos os dados em paralelo ========
export default function DashboardOptimizedPage() {
return (
<div>
<h1>Dashboard Otimizado</h1>
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: '1fr 1fr' }}>
<Suspense fallback={<Skeleton label="Usuários" />}>
<DataCard title="Usuários" endpoint="/users" />
</Suspense>
<Suspense fallback={<Skeleton label="Posts" />}>
<DataCard title="Posts" endpoint="/posts" revalidate={120} />
</Suspense>
<Suspense fallback={<Skeleton label="Comentários" />}>
<DataCard title="Comentários" endpoint="/comments" />
</Suspense>
<Suspense fallback={<Skeleton label="Tarefas" />}>
<DataCard title="Tarefas" endpoint="/todos" revalidate={30} />
</Suspense>
</div>
</div>
)
}
async function DataCard({ title, endpoint, revalidate }: {
title: string
endpoint: string
revalidate?: number
}) {
const data = await getData<any[]>(endpoint, revalidate
? { next: { revalidate } }
: { cache: 'no-store' }
)
return (
<div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
<h2>{title} <span style={{ fontSize: 14, color: '#666' }}>({data.length})</span></h2>
<ul>{data.slice(0, 5).map((item: any) => (
<li key={item.id}>{item.title ?? item.name ?? item.email}</li>
))}</ul>
</div>
)
}
function Skeleton({ label }: { label: string }) {
return <div style={{ border: '1px solid #eee', borderRadius: 8, padding: 16, opacity: 0.5 }}>
Carregando {label}...
</div>
}
// app/dashboard-optimized/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshSection(tag: string) {
revalidateTag(tag)
return { success: true }
}
❓ Perguntas Frequentes
P: As opções 'force-cache' e 'no-store' no fetch se comportam da mesma forma no modo de desenvolvimento e no modo de produção? R: Não, não se comportam. No modo de desenvolvimento (
npm run dev), oforce-cacheainda é buscado a cada requisição (para facilitar a depuração). O cache só entra em vigor no modo de produção (next startou após o build). Esta é uma decisão de design do Next.js — sempre buscar os dados mais recentes durante a fase de desenvolvimento.
P: Qual é a diferença entre
revalidateTagerevalidatePath? R:revalidateTaglimpa o cache por tag (para os mesmos dados em páginas diferentes), enquantorevalidatePathlimpa o cache por caminho (até o nível da página ou padrão de rota). O primeiro é adequado para controle refinado na camada de dados, enquanto o último é adequado para atualizações no nível da página. Recomendamos usarrevalidateTagsempre que possível.
P: Se duas requisições fetch usarem a mesma URL mas opções diferentes, elas compartilharão o cache? R: Não. A chave de cache é calculada com base na URL, método, headers e body. Requisições com a mesma URL mas configurações diferentes de
cacheounext.revalidateserão tratadas como entradas de cache diferentes.
P: Como o
Promise.alllida com uma requisição que falha? R:Promise.allé uma operação "tudo ou nada" — se qualquer requisição falhar, toda a promise é rejeitada. Se você precisar tratar erros individualmente, envolva cada chamadafetchemPromise.allSettledou um blocotry-catch. Um padrão comum éconst results = await Promise.all(urls.map(u => fetch(u).catch(() => null))).
P: Como o timeout do
fetché tratado no RSC? R: Ofetchnão tem um timeout embutido. Você pode envolvê-lo em umAbortController:const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 5000); fetch(url, { signal: ctrl.signal }). Recomenda-se encapsular um clientefetchunificado no nível da aplicação.
P: Posso usar um cliente HTTP de terceiros (como axios) no RSC? R: Sim, mas você perderá os recursos estendidos do
fetchdo Next.js, como cache automático, tags e revalidação. Se usar axios, você precisará implementar a lógica de cache manualmente ou envolver uma camada compatível comfetchem torno do axios. Recomendamos usar o métodofetchnativo sempre que possível.
📖 Resumo
- O método
fetchno RSC usaforce-cachepor padrão, e URLs idênticas são automaticamente cacheadas cache: 'no-store'Desabilita o cache; adequado para dados em tempo realnext: { revalidate: N }Implementa um cache de janela de tempo (semelhante ao ISR)next: { tags: [...] }+revalidateTag()implementa atualização de cache sob demandaPromise.all()Requisições paralelas evitam o "efeito cascata" e podem reduzir o tempo de carregamento em 60–80%- Suspense usa divisão por limites (boundary) para permitir carregamento em streaming, sem precisar esperar todos os dados ficarem prontos
revalidatePath()Limpa o cache no nível da página por caminho
📝 Exercícios
-
Exercício Básico (⭐): Crie um
app/time-demo/page.tsx, usecache: 'no-store'ecache: 'force-cache'para fazer requisições separadas à API World Time, compare a diferença entre os dois timestamps e verifique o comportamento de cache. -
Exercício Avançado (⭐⭐): Construa um
app/parallel-demo/page.tsxque usePromise.allpara buscar/users,/postse/comments(usando a API JSONPlaceholder), e renderize cada conjunto de dados dentro de um limite<Suspense>separado para demonstrar o efeito de carregamento em streaming. -
Desafio (⭐⭐⭐): Crie uma página de lista de tarefas com suporte a operações CRUD:
app/tasks/page.tsx(exibe a lista de tarefas, usando tags para cache) eapp/tasks/actions.ts(chamarevalidateTag('tasks')para atualizar a lista após adicionar ou excluir uma tarefa). Implemente uma atualização otimista para garantir que a lista seja atualizada imediatamente após uma operação de escrita.