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



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.

TSX
// 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.

100%
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).

TSX
// 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.

TSX
// 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:

TEXT 📖 Somente leitura
Renderiza a interface do componente AboutPage.
TSX
// 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:

TEXT 📖 Somente leitura
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

TSX
// 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.

TSX
// 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:

TEXT 📖 Somente leitura
Obtém dados e renderiza uma lista de itens.
TSX
// 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:

TEXT 📖 Somente leitura
Log de Build:
✓ Gerando páginas estáticas (5/5)  /products/1  /products/2  /products/3  /products/4  /products/5

Saída:

TEXT 📖 Somente leitura
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.

100%
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

TSX
// 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.

TSX
// 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:

TEXT 📖 Somente leitura
Renderiza a interface do componente updatePost.
TSX
// 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:

TEXT 📖 Somente leitura
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:

TEXT 📖 Somente leitura
A página renderiza conforme descrito acima, com a UI atualizando de acordo com o comportamento descrito.
TSX
// 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 })
}
BASH
# 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:

TEXT 📖 Somente leitura
{"status":"ok","data":{}}

▶ Exemplo: dynamic = 'force-static' para configurar uma página estática (Dificuldade: ⭐)

Saída:

TEXT 📖 Somente leitura
A página renderiza conforme descrito acima, com a UI atualizando de acordo com o comportamento descrito.
TSX
// 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:

TEXT 📖 Somente leitura
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.
TSX
// 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:

TEXT 📖 Somente leitura
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

TSX
// 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 generateStaticParams causará 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"). Use export 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-dynamic e cache: 'no-store'? R: force-dynamic é uma configuração de segmento de rota (a página inteira é dinâmica), enquanto cache: 'no-store' é uma configuração de busca de dados (buscas individuais não são cacheadas). force-dynamic desabilita todo o cache de dados para aquela página. Recomenda-se priorizar o ajuste fino de cache (como uma combinação de no-store e force-cache) em vez de usar force-dynamic como solução única.

P: Qual tem melhor desempenho, revalidatePath ou revalidateTag? R: revalidateTag é melhor — ele limpa apenas as entradas de cache que correspondem àquela tag e não afeta outros dados. revalidatePath requer percorrer todas as entradas de cache ao longo do caminho, o que gera maior sobrecarga. Recomendamos criar um sistema de tags significativo e usar tags para 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


📝 Exercícios

  1. Tarefa Básica (⭐): Crie um app/ssg-demo/[id]/page.tsx e use generateStaticParams para pré-gerar cinco páginas estáticas — /products/1 até /products/5 (dados da API FakeStore). Após o build, verifique se o HTML de cada página é um arquivo estático.

  2. Exercício Avançado (⭐⭐): Crie uma página de exibição de hora ISR app/isr-clock/page.tsx, configure revalidate = 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 Action forceRefresh() que usa revalidatePath() para atualizar a página imediatamente.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%