Next.js: Estratégias de Renderização: SSR, SSG & ISR
Última atualização: 2026-08-26
As estratégias de renderização do Next.js permitem que você escolha entre "pronto no momento do build" e "sob demanda" — a chave está em selecionar o modo certo.
1. O Que Você Vai Aprender
- Primeiro Byte em Tempo Real na Fronteira entre Renderização Streaming SSR e Suspense
- SSG Geração de Site Estático e
generateStaticParams()Pré-geração de Rotas Dinâmicas - ISR Regeneração Estática Incremental e a Janela de Tempo
revalidate force-dynamicvsforce-staticConfiguração de Segmento de RotarevalidatePath()/revalidateTag()Gatilho ISR sob demanda
2. Uma História Real de um Gerente Técnico
(1) Ponto de Dor: Páginas do CMS levam 6 segundos para carregar, e a equipe de operações as atualiza 50 vezes por dia
Charlie é o Líder Técnico da equipe TaskFlow. O CMS de e-commerce da empresa tem 10.000 páginas de produtos, cada uma contendo descrição, preço, inventário e imagens. O pior é:
| Problema | Dados |
|---|---|
| Tempo de resposta SSR por página | 6 segundos (cada requisição consulta o banco de dados) |
| Atualizações diárias da equipe de operações | 50+ atualizações (preços/inventário/promoções) |
| CPU do Servidor | Consistentemente 85%+ |
| Política de Cache | ❌ Nenhuma — Renderizado em tempo real a cada vez |
A equipe de operações atualiza os preços a cada 15 minutos, mas o SSR precisa consultar o banco de dados novamente a cada vez — 10.000 páginas × 6 segundos = 60.000 segundos de sobrecarga de CPU por dia.
(2) Soluções com ISR
Ao construir com ISR, gere páginas estáticas, revalide a cada N segundos e atualize imediatamente sob demanda.
// app/products/[id]/page.tsx — ISR Regeneração Estática Incremental
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { id: true } })
return products.map(p => ({ id: String(p.id) }))
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: Number(params.id) } })
return <ProductView product={product} />
}
// Configuração de Segmento de Rota
export const revalidate = 300 // Revalidar a cada 5 minutos
(3) Resultados
| Dimensão | SSR Puro | ISR |
|---|---|---|
| Tempo de Resposta | 6 segundos | < 50 ms (HTML estático) |
| CPU do Servidor | 85% | < 10% |
| Atualização Operacional | ❌ Deve esperar 6 segundos | ✅ Instantâneo (revalidação sob demanda) |
| Cache CDN | ❌ Não Suportado | ✅ Cache de Página Completa Suportado |
| QPS do Banco de Dados | 10.000/hora | ~50/hora |
3. SSR (Renderização no Lado do Servidor)
SSR (Server-Side Rendering) renderiza HTML no servidor para cada requisição. O Next.js 16 usa o streaming SSR do React 18 — em vez de esperar a página inteira terminar de renderizar, ele divide a página em múltiplos limites Suspense e os envia ao cliente um por um.
sequenceDiagram
participant Client as Navegador
participant Server as Servidor Next.js
participant DB as Banco de Dados
Client->>Server: GET /dashboard
Server->>Client: Enviar Shell HTML Estático (imediato)
Server->>DB: Consultar Dados (Paralelo)
DB-->>Server: Retornar um subconjunto dos dados
Server->>Client: Transmissão Streaming Limite 1 do <Suspense>
DB-->>Server: Carregar Mais Dados
Server->>Client: Transmissão Streaming Limite 2 do <Suspense>
Client->>Client: Renderizar Conteúdo Gradualmente
| Configuração | Sintaxe | Comportamento |
|---|---|---|
| Padrão (streaming SSR) | Sem configuração | Renderização dinâmica da página, suporta streaming Suspense |
force-dynamic |
export const dynamic = 'force-dynamic' |
Força uma nova renderização a cada requisição; desabilita cache |
force-static |
export const dynamic = 'force-static' |
Força compilação estática durante o build; desabilita comportamento dinâmico |
(1) dynamic = 'force-dynamic'
Garante que a página seja regenerada a cada requisição, adequado para dados altamente personalizados (dashboards de usuário, carrinhos de compras).
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'
export default async function DashboardPage() {
const user = await getCurrentUser() // Obtém os dados mais recentes a cada requisição
return <DashboardView user={user} />
}
(2) dynamic = 'force-static'
Força a renderização estática das páginas durante o build; mesmo que contenham funções dinâmicas, essas funções serão pré-computadas.
// app/about/page.tsx
export const dynamic = 'force-static'
export default async function AboutPage() {
const version = await getVersion() // Calculado uma vez durante o build
return <div>Versão: {version}</div>
}
▶ Exemplo: Experiência de Carregamento Streaming SSR (Dificuldade: ⭐)
Saída:
Renderiza a interface do componente AboutPage.
// app/streaming-demo/page.tsx
import { Suspense } from 'react'
export default function StreamingDemoPage() {
return (
<div>
<h1>Demo de Streaming SSR</h1>
<p>Este texto aparece imediatamente (shell estático).</p>
<Suspense fallback={<div>Carregando dados lentos...</div>}>
<SlowComponent delay={3000} />
</Suspense>
<Suspense fallback={<div>Carregando dados rápidos...</div>}>
<SlowComponent delay={1000} />
</Suspense>
</div>
)
}
async function SlowComponent({ delay }: { delay: number }) {
await new Promise(resolve => setTimeout(resolve, delay))
return <div>Carregado após {delay}ms</div>
}
Saída:
Renderiza um shell estático imediatamente, com conteúdo dinâmico carregando dentro dos limites do Suspense.
Fallback: Carregando dados lentos...
Texto visível: Demo de Streaming SSR | Este texto aparece imediatamente (shell estático). | Carregando dados lentos... | }>
4. SSG Geração de Site Estático e generateStaticParams
SSG (Static Site Generation) gera todas as páginas HTML de uma vez durante o processo de build, sendo adequado para cenários onde o conteúdo muda com pouca frequência (blogs, documentação, páginas de marketing).
(1) Página Estática
// app/docs/page.tsx — Gerada durante o build
export default async function DocsPage() {
const docs = await fetch('https://cms.example.com/docs', {
cache: 'force-cache' // Obtido durante o build, Cache Persistente
}).then(r => r.json())
return <ArticleView content={docs} />
}
(2) generateStaticParams() Pré-geração de Rotas Dinâmicas
Usado para páginas como [id] e [slug] que usam roteamento dinâmico; retorna todos os parâmetros de caminho possíveis, gerados uma vez durante o build.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://cms.example.com/posts').then(r => r.json())
return posts.map((post: any) => ({ slug: post.slug })) // → /blog/hello-world, /blog/nextjs-guide ...
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.slug}`).then(r => r.json())
return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}
▶ Exemplo: Pré-geração de Roteamento Dinâmico SSG (Dificuldade: ⭐⭐)
Saída:
Obtém dados e renderiza uma lista de itens.
// app/products/[id]/page.tsx
type Product = { id: number; title: string; price: number }
export async function generateStaticParams() {
const products: Product[] = await fetch('https://fakestoreapi.com/products').then(r => r.json())
return products.slice(0, 5).map(p => ({ id: String(p.id) }))
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product: Product = await fetch(`https://fakestoreapi.com/products/${params.id}`).then(r => r.json())
return (
<div>
<h1>{product.title}</h1>
<p>Preço: ${product.price}</p>
</div>
)
}
Saída:
Log de Build:
✓ Gerando páginas estáticas (5/5) /products/1 /products/2 /products/3 /products/4 /products/5
Saída:
O navegador renderiza cada página de produto:
/products/1 → Título do produto + "Preço: $109.95"
/products/2 → Título do produto + "Preço: $22.30"
/products/3 → Título do produto + "Preço: $55.99"
/products/4 → Título do produto + "Preço: $15.99"
/products/5 → Título do produto + "Preço: $695.00"
5. ISR Regeneração Estática Incremental
ISR (Incremental Static Regeneration) fica entre SSG e SSR: páginas estáticas são geradas durante o build, e a regeneração é disparada em segundo plano após a janela de revalidate expirar, para que os usuários sempre vejam páginas em cache.
graph LR
A[Build] --> B[Gerar HTML Estático]
B --> C[Cache CDN]
C --> D[Requisição do Usuário]
D --> E{revalidate<br/>Expirado?}
E -->|Não| F[Retornar Cache]
E -->|Sim| G[Retornar Cache + Regenerar em segundo plano]
G --> B
| Modo | revalidate | Comportamento | Primeira Visita | Atraso de Atualização |
|---|---|---|---|---|
| SSG | Nenhum | Gerado apenas durante o build | Instantâneo | Requer rebuild |
| ISR | revalidate: 60 |
Atualiza em segundo plano após 60 segundos | Tempo real | ≤ 60 segundos |
| SSR | dynamic: 'force-dynamic' |
Atualiza a cada requisição | Gerado dinamicamente | Tempo real |
(1) Configuração de Segmento de Rota com Revalidate
// app/posts/[id]/page.tsx
export const revalidate = 3600 // Revalidar uma vez por hora
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.id}`, {
next: { revalidate: 3600 } // Também compatível no nível do fetch
}).then(r => r.json())
return <PostView post={post} />
}
(2) ISR Sob Demanda (Revalidação Sob Demanda)
Use revalidatePath() ou revalidateTag() para disparar regeneração imediata quando os dados mudam.
// app/admin/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function updatePost(formData: FormData) {
const id = formData.get('id') as string
await db.post.update({ where: { id: Number(id) }, data: { title: formData.get('title') as string } })
revalidatePath(`/posts/${id}`) // Atualiza um Artigo Específico
revalidatePath('/posts') // Atualiza a página de listagem
revalidateTag('posts') // Atualiza todos os caches com a tag posts
}
▶ Exemplo: Janela de Tempo ISR (Dificuldade ⭐⭐)
Saída:
Renderiza a interface do componente updatePost.
// app/isr-demo/page.tsx
export const revalidate = 30 // Revalidação a cada 30 segundos
export default async function IsrDemoPage() {
const time = await fetch('http://worldtimeapi.org/api/timezone/Asia/Shanghai', {
next: { tags: ['time'] }
}).then(r => r.json())
return (
<div>
<h1>Demo ISR — Revalida a cada 30s</h1>
<p>Hora atual: {time.datetime}</p>
<p>Gerado em: {new Date().toISOString()}</p>
</div>
)
}
Saída:
Obtém dados e renderiza o resultado.
Texto visível: Demo ISR — Revalida a cada 30s | Hora atual: {time.datetime}
▶ Exemplo: Revalidando uma Rota de API Sob Demanda (Dificuldade: ⭐⭐⭐)
Saída:
A página renderiza conforme descrito acima, com a UI atualizando de acordo com o comportamento descrito.
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Segredo inválido' }, { status: 401 })
}
const body = await request.json()
if (body.type === 'path') {
revalidatePath(body.path)
} else if (body.type === 'tag') {
revalidateTag(body.tag)
}
return NextResponse.json({ revalidated: true })
}
# Chamada via Webhook do CMS
curl -X POST https://example.com/api/revalidate \
-H "x-revalidate-secret: your-secret" \
-H "Content-Type: application/json" \
-d '{"type": "tag", "tag": "posts"}'
Saída:
{"status":"ok","data":{}}
▶ Exemplo: dynamic = 'force-static' para configurar uma página estática (Dificuldade: ⭐)
Saída:
A página renderiza conforme descrito acima, com a UI atualizando de acordo com o comportamento descrito.
// app/static-page/page.tsx — Força Estático
export const dynamic = 'force-static'
export default async function StaticPage() {
const time = new Date().toISOString()
return (
<div>
<h1>Página Estática (construída no momento do build)</h1>
<p>Este timestamp é fixo: {time}</p>
<p>Atualize a página — o tempo nunca muda.</p>
</div>
)
}
Saída:
Renderiza: Página Estática (construída no momento do build)
Texto visível: Página Estática (construída no momento do build) | Este timestamp é fixo: {time} | Atualize a página — o tempo nunca muda.
// app/dynamic-page/page.tsx — Atualização Obrigatória
export const dynamic = 'force-dynamic'
export default async function DynamicPage() {
const time = new Date().toISOString()
return (
<div>
<h1>Página Dinâmica (renderizada por requisição)</h1>
<p>Este timestamp atualiza a cada atualização: {time}</p>
</div>
)
}
Comparação de Saída:
Página estática: Timestamp sempre "2026-07-06T10:00:00.000Z" ← Não muda
Página dinâmica: Timestamp muda a cada atualização ← É diferente a cada vez
6. Exemplo Completo: Sistema de Blog com Três Modos
// app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return <div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}>{children}</div>
}
// app/blog/page.tsx — Lista de Blog (SSG + ISR)
export const revalidate = 300 // ISR de 5 minutos
export default async function BlogListPage() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { tags: ['blog-posts'] }
}).then(r => r.json())
return (
<div>
<h1>Blog</h1>
<ul>{posts.slice(0, 10).map((p: any) => (
<li key={p.id}><a href={`/blog/${p.id}`}>{p.title}</a></li>
))}</ul>
<p>Última geração: {new Date().toISOString()}</p>
</div>
)
}
// app/blog/[id]/page.tsx — Detalhes do Artigo (SSG + ISR)
export async function generateStaticParams() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json())
return posts.slice(0, 10).map((p: any) => ({ id: String(p.id) }))
}
export default async function BlogPostPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`, {
next: { tags: [`post-${params.id}`] }
}).then(r => r.json())
const comments = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}/comments`, {
cache: 'no-store'
}).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<h2>Comentários ({comments.length})</h2>
<ul>{comments.map((c: any) => (
<li key={c.id}><strong>{c.name}:</strong> {c.body}</li>
))}</ul>
</article>
)
}
// app/blog/actions.ts — Atualizar Sob Demanda
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshBlog() {
revalidateTag('blog-posts')
}
// app/blog/[id]/admin/page.tsx — Painel de Administração (SSR)
export const dynamic = 'force-dynamic'
export default async function AdminPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`).then(r => r.json())
return (
<form action={async (fd) => {
'use server'
await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`, { method: 'PATCH', body: JSON.stringify({ title: fd.get('title') }) })
revalidateTag(`post-${params.id}`)
}}>
<input name="title" defaultValue={post.title} />
<button type="submit">Atualizar</button>
</form>
)
}
❓ Perguntas Frequentes
P: Qual é a diferença entre SSG e ISR? R: SSG é gerado apenas uma vez durante o processo de build; atualizações exigem um rebuild. ISR é gerado durante o build e é automaticamente regenerado em segundo plano quando a janela de revalidação expira. ISR = SSG + atualizações automáticas. ISR é mais adequado para cenários onde o conteúdo é atualizado com frequência.
P: O
generateStaticParamscausará um build muito demorado se gerar um grande número de páginas? R: Sim. 10.000 páginas × 1 segundo cada = 10.000 segundos (~2,8 horas). Solução: Gere apenas páginas populares (ex.: as 100 principais) e execute SSG sob demanda para páginas não geradas na primeira visita (conhecido como "ISR sob demanda"). Useexport const dynamicParams = true(padrão) para permitir renderização dinâmica de caminhos não pré-gerados.
P: Como os limites Suspense funcionam no SSR? R: Cada limite
<Suspense>é uma unidade independente de saída em streaming. O servidor primeiro envia um shell HTML estático, depois envia progressivamente o conteúdo de cada limite Suspense. Os usuários veem um efeito de "carregamento progressivo" — não precisam esperar todos os dados ficarem prontos.
P: Qual é a diferença entre
force-dynamicecache: 'no-store'? R:force-dynamicé uma configuração de segmento de rota (a página inteira é dinâmica), enquantocache: 'no-store'é uma configuração de busca de dados (buscas individuais não são cacheadas).force-dynamicdesabilita todo o cache de dados para aquela página. Recomenda-se priorizar o ajuste fino de cache (como uma combinação deno-storeeforce-cache) em vez de usarforce-dynamiccomo solução única.
P: Qual tem melhor desempenho,
revalidatePathourevalidateTag? R:revalidateTagé melhor — ele limpa apenas as entradas de cache que correspondem àquela tag e não afeta outros dados.revalidatePathrequer percorrer todas as entradas de cache ao longo do caminho, o que gera maior sobrecarga. Recomendamos criar um sistema de tags significativo e usartagspara controle refinado.
P: O que significa "stale-while-revalidate" no ISR? R: Modo stale-while-revalidate: Quando um usuário faz uma requisição, se o cache expirou, o sistema retorna imediatamente a página antiga em cache enquanto dispara uma regeneração em segundo plano. O próximo usuário verá então a nova versão. Dessa forma, os usuários nunca precisam esperar a página ser gerada, mas pode haver um breve atraso nos dados.
📖 Resumo
- Renderização streaming SSR alcança primeiro byte instantâneo e carregamento progressivo através de limites Suspense
dynamic = 'force-static'/'force-dynamic'Controla o modo de renderização da páginagenerateStaticParams()Pré-gera todas as combinações de páginas para roteamento dinâmico durante o build- ISR implementa atualizações incrementais automáticas para páginas estáticas usando
revalidate = N - ISR Sob Demanda usa
revalidatePath()/revalidateTag()para atualizar imediatamente quando os dados mudam - Pirâmide de Seleção de Estratégia de Renderização: SSG (não muda) → ISR (muda ocasionalmente) → SSR (muda em tempo real) → PPR (muda parcialmente)
📝 Exercícios
-
Tarefa Básica (⭐): Crie um
app/ssg-demo/[id]/page.tsxe usegenerateStaticParamspara pré-gerar cinco páginas estáticas —/products/1até/products/5(dados da API FakeStore). Após o build, verifique se o HTML de cada página é um arquivo estático. -
Exercício Avançado (⭐⭐): Crie uma página de exibição de hora ISR
app/isr-clock/page.tsx, configurerevalidate = 15, e use a API World Time para exibir a hora atual. Atualize a página a cada 15 segundos para verificar se a hora foi atualizada. Adicione uma Server ActionforceRefresh()que usarevalidatePath()para atualizar a página imediatamente. -
Desafio (⭐⭐⭐): Construa um sistema de blog completo:
app/blog/page.tsx(lista ISR, revalidada a cada 120 segundos),app/blog/[slug]/page.tsx(metadados SEO + dados estruturados JSON-LD),app/api/revalidate/route.ts(endpoint Webhook para receber notificações de mudança do CMS, atualizar cache por tag). Forneça um script de exemplo para uma chamada Webhook.