Cache Components e Partial Prerendering no Next.js 16: Guia Prático Completo

Guia prático sobre Cache Components e Partial Prerendering no Next.js 16. Domine a diretiva "use cache", cacheLife, cacheTag e PPR com exemplos de e-commerce, dashboards e blogs. Inclui migração do Next.js 15.

Next.js 16 Cache & PPR: Guia Completo 2026

Introdução: A Nova Era do Cache no Next.js 16

Se você já trabalhou com Next.js 15 ou versões anteriores, provavelmente já passou por aquela situação frustrante: dados sendo cacheados quando você não esperava, páginas estáticas quando deveriam ser dinâmicas, e aquela sensação constante de que o framework tava tomando decisões por você nos bastidores. Confesso que já perdi horas debugando problemas que, no fim das contas, eram só o cache implícito fazendo das suas.

Pois bem, o Next.js 16 chegou pra mudar fundamentalmente essa relação. A equipe da Vercel ouviu a comunidade e fez uma das mudanças mais significativas da história do framework: o cache agora é explícito, não implícito.

Na prática, isso significa que todo código em páginas, layouts e rotas de API é dinâmico por padrão. Nada é cacheado a menos que você explicitamente peça isso. Essa mudança filosófica elimina uma categoria inteira de bugs e confusões que atormentavam desenvolvedores. Mas, como sempre, com grande poder vem grande responsabilidade — agora você precisa entender as novas ferramentas de cache pra extrair o máximo de performance.

Neste guia, vamos explorar todas as novidades do sistema de cache do Next.js 16: a diretiva use cache, a configuração cacheComponents, as funções cacheLife e cacheTag, o Partial Prerendering (PPR) e mais. Tudo com exemplos práticos que você pode aplicar nos seus projetos hoje mesmo.

Entendendo a Diretiva use cache

A diretiva use cache é o coração do novo sistema de cache. Inspirada no padrão de diretivas do React (como use client e use server), ela permite que você declare explicitamente quais partes da aplicação devem ser cacheadas. E o mais legal: pode ser aplicada em três níveis distintos.

Nível de Arquivo (Page Level)

Quando você coloca "use cache" no topo de um arquivo, a página ou layout inteiro será cacheado. É basicamente o comportamento estático das versões anteriores, mas agora com controle total sobre quando e como esse cache funciona.

// app/sobre/page.tsx
"use cache"

export default async function SobrePage() {
  const empresa = await fetch('https://api.exemplo.com/empresa').then(r => r.json())

  return (
    <main>
      <h1>{empresa.nome}</h1>
      <p>{empresa.descricao}</p>
      <p>Fundada em {empresa.anoFundacao}</p>
    </main>
  )
}

Neste exemplo, a página inteira é renderizada uma vez e servida do cache nas requisições seguintes. Ideal pra páginas que raramente mudam — institucional, políticas de privacidade, documentação, esse tipo de coisa.

Nível de Componente (Component Level)

Aqui é onde as coisas ficam realmente interessantes. Você pode cachear componentes individuais dentro de uma página que é majoritariamente dinâmica. A diretiva "use cache" vai dentro do corpo de um componente assíncrono.

// app/components/CategoriasSidebar.tsx
async function CategoriasSidebar() {
  "use cache"

  const categorias = await fetch('https://api.exemplo.com/categorias').then(r => r.json())

  return (
    <aside>
      <h2>Categorias</h2>
      <ul>
        {categorias.map((cat: { id: string; nome: string }) => (
          <li key={cat.id}>
            <a href={`/categoria/${cat.id}`}>{cat.nome}</a>
          </li>
        ))}
      </ul>
    </aside>
  )
}

export default CategoriasSidebar

Esse componente será cacheado independentemente da página onde é usado. Mesmo com a página principal sendo dinâmica, a sidebar de categorias é servida instantaneamente do cache. Bem elegante, né?

Nível de Função (Function Level)

Pra granularidade máxima, você pode cachear funções individuais. Isso é perfeito pra funções de busca de dados reutilizadas em vários lugares da aplicação.

// lib/dados.ts
export async function buscarConfiguracoesGlobais() {
  "use cache"

  const config = await fetch('https://api.exemplo.com/config').then(r => r.json())
  return config
}

export async function buscarMenuNavegacao() {
  "use cache"

  const menu = await fetch('https://api.exemplo.com/menu').then(r => r.json())
  return menu
}

A grande vantagem aqui é a reutilização. Não importa quantos componentes chamem buscarConfiguracoesGlobais() — o resultado é computado apenas uma vez e servido do cache pra todas as chamadas subsequentes.

Importante: A diretiva "use cache" só funciona em funções e componentes assíncronos (async). Componentes de cliente ("use client") não podem usar "use cache" diretamente — o cache deve ser aplicado no componente servidor que fornece os dados.

Configuração com cacheComponents: true

Pra habilitar o novo sistema de cache, você precisa configurar a aplicação corretamente. Se você vinha usando a flag experimental dynamicIO no Next.js 15, a migração é simples: essa configuração foi substituída pela opção estável cacheComponents.

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

Essa única linha habilita todo o sistema de cache baseado em diretivas. Sem ela, a diretiva "use cache" é silenciosamente ignorada (e isso já me pegou de surpresa mais de uma vez).

Comparação: Antes vs. Depois

Veja como a configuração mudou entre as versões:

// Next.js 15 (experimental)
// next.config.ts
const nextConfig = {
  experimental: {
    dynamicIO: true,
  },
}

// Next.js 16 (estável)
// next.config.ts
const nextConfig = {
  cacheComponents: true,
}

Além da mudança de nome, a grande diferença é que cacheComponents saiu de baixo da chave experimental. Ou seja, a API é considerada estável e você pode confiar nela pra produção sem medo de breaking changes em minor versions. Finalmente.

O que muda com cacheComponents: true

  • Todo código é dinâmico por padrão — páginas, layouts, componentes e funções são executados a cada requisição, a menos que você use "use cache".
  • Fetch não é mais cacheado automaticamente — diferente do Next.js 14 onde fetch() era cacheado por padrão, agora cada chamada é executada a cada requisição.
  • Controle granular de cache — você pode misturar conteúdo estático e dinâmico na mesma página com precisão cirúrgica.
  • As funções cacheLife e cacheTag ficam disponíveis — sem a configuração habilitada, essas funções simplesmente não têm efeito.

Função cacheLife: Controlando a Duração do Cache

Cachear dados é ótimo, mas por quanto tempo? A função cacheLife permite que você defina com precisão a duração do cache pra cada entrada. Ela aceita presets predefinidos e também configurações customizadas.

Presets Disponíveis

O Next.js 16 oferece sete presets que cobrem os cenários mais comuns:

  • 'default' — duração moderada, adequada pra maioria dos casos
  • 'seconds' — cache de poucos segundos, pra dados que mudam frequentemente
  • 'minutes' — cache de minutos, bom pra APIs com rate limiting
  • 'hours' — cache de horas, ideal pra dados que mudam algumas vezes ao dia
  • 'days' — cache de dias, pra conteúdo que muda raramente
  • 'weeks' — cache de semanas, pra conteúdo quase estático
  • 'max' — cache máximo possível, pra conteúdo verdadeiramente estático
import { cacheLife } from 'next/cache'

async function ProdutosDestaque() {
  "use cache"
  cacheLife('hours')

  const produtos = await fetch('https://api.loja.com/destaques').then(r => r.json())

  return (
    <section>
      <h2>Produtos em Destaque</h2>
      {produtos.map((p: { id: string; nome: string; preco: number }) => (
        <div key={p.id}>
          <h3>{p.nome}</h3>
          <p>R$ {p.preco.toFixed(2)}</p>
        </div>
      ))}
    </section>
  )
}

Configuração Customizada

Quando os presets não atendem, você pode definir configurações customizadas diretamente no next.config.ts e usá-las por nome.

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    catalogo: {
      stale: 3600,    // 1 hora - tempo que o dado é considerado "fresco"
      revalidate: 900, // 15 min - intervalo de revalidação em segundo plano
      expire: 86400,   // 24 horas - tempo máximo antes de forçar nova geração
    },
    precos: {
      stale: 60,       // 1 minuto
      revalidate: 30,  // 30 segundos
      expire: 300,     // 5 minutos
    },
  },
}

export default nextConfig

Com essas configurações definidas, usar fica bem direto:

import { cacheLife } from 'next/cache'

async function buscarPrecos(produtoId: string) {
  "use cache"
  cacheLife('precos')

  const preco = await fetch(`https://api.loja.com/precos/${produtoId}`).then(r => r.json())
  return preco
}

async function buscarCatalogo() {
  "use cache"
  cacheLife('catalogo')

  const catalogo = await fetch('https://api.loja.com/catalogo').then(r => r.json())
  return catalogo
}

A configuração customizada oferece três parâmetros fundamentais que trabalham juntos seguindo o modelo stale-while-revalidate (SWR):

  1. stale — período em que o conteúdo é servido diretamente do cache, sem nenhuma verificação.
  2. revalidate — intervalo em que o Next.js revalida o cache em segundo plano, servindo o conteúdo antigo enquanto gera o novo.
  3. expire — tempo máximo absoluto de vida da entrada no cache. Depois desse período, o conteúdo é regenerado obrigatoriamente.

cacheTag e revalidateTag/updateTag: Invalidação Sob Demanda

O cache baseado em tempo é poderoso, mas muitas vezes você precisa invalidar o cache na hora quando algo muda. É aí que entram as tags de cache. E a boa notícia: diferente do Next.js 15, onde essas funções tinham o prefixo unstable_, no Next.js 16 elas são estáveis e podem ser importadas diretamente de next/cache.

Marcando Entradas com cacheTag

import { cacheLife, cacheTag } from 'next/cache'

async function DetalhesProduto({ produtoId }: { produtoId: string }) {
  "use cache"
  cacheLife('days')
  cacheTag(`produto-${produtoId}`, 'produtos')

  const produto = await fetch(`https://api.loja.com/produtos/${produtoId}`).then(r => r.json())

  return (
    <div>
      <h1>{produto.nome}</h1>
      <p>{produto.descricao}</p>
      <p>R$ {produto.preco.toFixed(2)}</p>
      <p>Estoque: {produto.estoque} unidades</p>
    </div>
  )
}

Repare que cacheTag aceita múltiplas tags. Neste exemplo, o componente está marcado com uma tag específica (produto-123) e uma genérica (produtos). Isso permite invalidação tanto granular quanto em massa — o que na prática é muito útil.

Invalidando com revalidateTag

A função revalidateTag serve pra invalidar entradas de cache sob demanda, tipicamente em resposta a webhooks, Server Actions ou rotas de API.

// app/api/webhook/produto/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const { produtoId, evento } = await request.json()

  if (evento === 'produto.atualizado') {
    revalidateTag(`produto-${produtoId}`)
  }

  if (evento === 'catalogo.atualizado') {
    revalidateTag('produtos')
  }

  return NextResponse.json({ revalidado: true })
}

O revalidateTag também aceita um segundo argumento opcional que define o comportamento de expiração. Ao passar 'max', a revalidação utiliza o modelo SWR — o conteúdo antigo continua sendo servido enquanto o novo é gerado em segundo plano:

// Revalidação com comportamento SWR
revalidateTag('produto-123', 'max')

Atualizando com updateTag em Server Actions

Agora, a função updateTag é exclusiva pra Server Actions e traz um comportamento especial chamado read-your-own-writes. Basicamente, quando você usa updateTag, o cache é expirado imediatamente e o usuário que disparou a ação vê os dados atualizados na próxima navegação, sem nenhum atraso.

Honestamente, isso é um divisor de águas pra UX.

// app/actions/produto.ts
"use server"

import { updateTag } from 'next/cache'

export async function atualizarProduto(formData: FormData) {
  const produtoId = formData.get('produtoId') as string
  const novoNome = formData.get('nome') as string
  const novoPreco = parseFloat(formData.get('preco') as string)

  await fetch(`https://api.loja.com/produtos/${produtoId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ nome: novoNome, preco: novoPreco }),
  })

  updateTag(`produto-${produtoId}`)
}

Diferença crucial: revalidateTag marca o cache como "stale" e revalida em segundo plano (SWR). Já o updateTag expira o cache imediatamente e garante que a próxima leitura retorne dados frescos. Use updateTag quando o usuário precisa ver o resultado da própria ação instantaneamente.

Partial Prerendering (PPR): O Melhor dos Dois Mundos

O Partial Prerendering é, sem dúvida, uma das funcionalidades mais empolgantes do Next.js 16. Ele resolve um problema antigo do desenvolvimento web: a escolha binária entre páginas estáticas (rápidas mas sem personalização) e páginas dinâmicas (personalizadas mas lentas).

Com PPR, você não precisa mais escolher.

Como o PPR Funciona

O conceito é elegante na sua simplicidade: o Next.js gera uma casca estática (static shell) da página no momento do build. Essa casca contém todo o conteúdo que pode ser pré-renderizado. As partes dinâmicas são marcadas com <Suspense> boundaries e seus fallbacks ficam incluídos no HTML estático.

Quando um usuário acessa a página, recebe instantaneamente o HTML estático com os fallbacks visíveis. Em paralelo, o servidor processa as partes dinâmicas e transmite via streaming pro navegador, que substitui os fallbacks pelo conteúdo real conforme ele chega. A experiência é surpreendentemente fluida.

// app/loja/page.tsx
import { Suspense } from 'react'
import CabecalhoLoja from '@/components/CabecalhoLoja'
import ProdutosDestaque from '@/components/ProdutosDestaque'
import CarrinhoResumo from '@/components/CarrinhoResumo'
import RecomendacoesPersonalizadas from '@/components/RecomendacoesPersonalizadas'

export default function LojaPage() {
  return (
    <main>
      {/* Estático — cacheado com "use cache" */}
      <CabecalhoLoja />

      {/* Estático — cacheado com "use cache" */}
      <ProdutosDestaque />

      {/* Dinâmico — personalizado por usuário */}
      <Suspense fallback={<div>Carregando seu carrinho...</div>}>
        <CarrinhoResumo />
      </Suspense>

      {/* Dinâmico — baseado em histórico do usuário */}
      <Suspense fallback={<div>Preparando recomendações...</div>}>
        <RecomendacoesPersonalizadas />
      </Suspense>
    </main>
  )
}

Neste exemplo, CabecalhoLoja e ProdutosDestaque são componentes cacheados que fazem parte da casca estática. Já o CarrinhoResumo e RecomendacoesPersonalizadas são dinâmicos, cada um com seu Suspense boundary e fallback adequado.

O Fluxo de Renderização

  1. Build time: O Next.js renderiza a casca estática com os componentes cacheados e os fallbacks dos Suspense boundaries.
  2. Requisição do usuário: O servidor envia imediatamente o HTML estático (TTFB extremamente baixo).
  3. Streaming: O servidor processa os componentes dinâmicos e transmite o HTML via streaming.
  4. Hidratação: O React no cliente substitui os fallbacks pelo conteúdo dinâmico conforme ele chega.

O resultado? Uma experiência que combina o TTFB de uma página estática com a personalização de uma página dinâmica. É, de verdade, o melhor dos dois mundos.

Exemplos Práticos

Exemplo 1: Página de Produto E-commerce

Vamos construir uma página de produto completa que demonstra a combinação de cache em múltiplos níveis com PPR. Esse é provavelmente o caso de uso mais comum que você vai encontrar.

// app/produto/[slug]/page.tsx
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
import InfoProduto from '@/components/InfoProduto'
import AvaliacoesProduto from '@/components/AvaliacoesProduto'
import PrecoDinamico from '@/components/PrecoDinamico'
import EstoqueTempoReal from '@/components/EstoqueTempoReal'
import ProdutosRelacionados from '@/components/ProdutosRelacionados'

interface PageProps {
  params: Promise<{ slug: string }>
}

export default async function ProdutoPage({ params }: PageProps) {
  const { slug } = await params

  return (
    <main>
      <Suspense fallback={<div className="skeleton-produto" />}>
        <InfoProduto slug={slug} />
      </Suspense>

      <Suspense fallback={<div className="skeleton-preco" />}>
        <PrecoDinamico slug={slug} />
      </Suspense>

      <Suspense fallback={<p>Verificando disponibilidade...</p>}>
        <EstoqueTempoReal slug={slug} />
      </Suspense>

      <Suspense fallback={<div className="skeleton-avaliacoes" />}>
        <AvaliacoesProduto slug={slug} />
      </Suspense>

      <Suspense fallback={<div className="skeleton-relacionados" />}>
        <ProdutosRelacionados slug={slug} />
      </Suspense>
    </main>
  )
}

Agora vamos ver cada componente:

// components/InfoProduto.tsx
import { cacheLife, cacheTag } from 'next/cache'

interface Produto {
  nome: string
  descricao: string
  imagens: string[]
  especificacoes: Record<string, string>
}

export default async function InfoProduto({ slug }: { slug: string }) {
  "use cache"
  cacheLife('days')
  cacheTag(`produto-info-${slug}`, 'produtos')

  const produto: Produto = await fetch(
    `https://api.loja.com/produtos/${slug}`
  ).then(r => r.json())

  return (
    <section>
      <h1>{produto.nome}</h1>
      <div className="galeria">
        {produto.imagens.map((img, i) => (
          <img key={i} src={img} alt={`${produto.nome} - imagem ${i + 1}`} />
        ))}
      </div>
      <p>{produto.descricao}</p>
      <table>
        <tbody>
          {Object.entries(produto.especificacoes).map(([chave, valor]) => (
            <tr key={chave}>
              <td><strong>{chave}</strong></td>
              <td>{valor}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </section>
  )
}
// components/PrecoDinamico.tsx
import { cookies } from 'next/headers'

export default async function PrecoDinamico({ slug }: { slug: string }) {
  const cookieStore = await cookies()
  const regiao = cookieStore.get('regiao')?.value || 'sudeste'

  const preco = await fetch(
    `https://api.loja.com/precos/${slug}?regiao=${regiao}`,
    { cache: 'no-store' }
  ).then(r => r.json())

  return (
    <div className="preco-container">
      {preco.promocao && (
        <span className="preco-original">
          De: R$ {preco.original.toFixed(2)}
        </span>
      )}
      <span className="preco-atual">
        R$ {preco.atual.toFixed(2)}
      </span>
      {preco.parcelas > 1 && (
        <span className="parcelas">
          ou {preco.parcelas}x de R$ {(preco.atual / preco.parcelas).toFixed(2)}
        </span>
      )}
    </div>
  )
}
// components/AvaliacoesProduto.tsx
import { cacheLife, cacheTag } from 'next/cache'

interface Avaliacao {
  id: string
  autor: string
  nota: number
  comentario: string
  data: string
}

export default async function AvaliacoesProduto({ slug }: { slug: string }) {
  "use cache"
  cacheLife('hours')
  cacheTag(`avaliacoes-${slug}`)

  const avaliacoes: Avaliacao[] = await fetch(
    `https://api.loja.com/produtos/${slug}/avaliacoes`
  ).then(r => r.json())

  const mediaNotas = avaliacoes.reduce((acc, a) => acc + a.nota, 0) / avaliacoes.length

  return (
    <section>
      <h2>Avaliações dos Clientes</h2>
      <p>Nota média: {mediaNotas.toFixed(1)} / 5 ({avaliacoes.length} avaliações)</p>
      {avaliacoes.slice(0, 5).map(av => (
        <article key={av.id}>
          <strong>{av.autor}</strong> — {'⭐'.repeat(av.nota)}
          <p>{av.comentario}</p>
          <time>{new Date(av.data).toLocaleDateString('pt-BR')}</time>
        </article>
      ))}
    </section>
  )
}

Exemplo 2: Dashboard com Conteúdo Misto

Dashboards são o caso de uso perfeito pro PPR. Normalmente combinam dados estáticos (menus, configurações) com dados em tempo real (métricas, gráficos). Vamos ver como fica na prática.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import MenuLateral from '@/components/dashboard/MenuLateral'
import ResumoVendas from '@/components/dashboard/ResumoVendas'
import GraficoTempoReal from '@/components/dashboard/GraficoTempoReal'
import UltimosPedidos from '@/components/dashboard/UltimosPedidos'
import MetasEquipe from '@/components/dashboard/MetasEquipe'

export default function DashboardPage() {
  return (
    <div className="dashboard-layout">
      <MenuLateral />

      <div className="dashboard-conteudo">
        <Suspense fallback={<div className="skeleton-resumo" />}>
          <ResumoVendas />
        </Suspense>

        <div className="dashboard-grid">
          <Suspense fallback={<div className="skeleton-grafico" />}>
            <GraficoTempoReal />
          </Suspense>

          <Suspense fallback={<div className="skeleton-pedidos" />}>
            <UltimosPedidos />
          </Suspense>
        </div>

        <Suspense fallback={<div className="skeleton-metas" />}>
          <MetasEquipe />
        </Suspense>
      </div>
    </div>
  )
}
// components/dashboard/ResumoVendas.tsx
import { cacheLife, cacheTag } from 'next/cache'

interface DadosResumo {
  vendasHoje: number
  receitaHoje: number
  ticketMedio: number
  comparativoOntem: number
}

export default async function ResumoVendas() {
  "use cache"
  cacheLife('minutes')
  cacheTag('dashboard-resumo')

  const dados: DadosResumo = await fetch(
    'https://api.interno.com/dashboard/resumo'
  ).then(r => r.json())

  return (
    <div className="resumo-cards">
      <div className="card">
        <h3>Vendas Hoje</h3>
        <p className="valor">{dados.vendasHoje}</p>
      </div>
      <div className="card">
        <h3>Receita</h3>
        <p className="valor">R$ {dados.receitaHoje.toLocaleString('pt-BR')}</p>
      </div>
      <div className="card">
        <h3>Ticket Médio</h3>
        <p className="valor">R$ {dados.ticketMedio.toFixed(2)}</p>
      </div>
      <div className="card">
        <h3>vs. Ontem</h3>
        <p className={dados.comparativoOntem >= 0 ? 'positivo' : 'negativo'}>
          {dados.comparativoOntem >= 0 ? '+' : ''}{dados.comparativoOntem}%
        </p>
      </div>
    </div>
  )
}
// components/dashboard/GraficoTempoReal.tsx
import { cookies } from 'next/headers'

export default async function GraficoTempoReal() {
  const cookieStore = await cookies()
  const token = cookieStore.get('auth-token')?.value

  const vendas = await fetch('https://api.interno.com/dashboard/vendas-hora', {
    headers: { Authorization: `Bearer ${token}` },
    cache: 'no-store',
  }).then(r => r.json())

  return (
    <div className="grafico-container">
      <h3>Vendas por Hora</h3>
      <div className="barras">
        {vendas.map((v: { hora: string; total: number; max: number }) => (
          <div
            key={v.hora}
            className="barra"
            style={{ height: `${(v.total / v.max) * 100}%` }}
            title={`${v.hora}: ${v.total} vendas`}
          />
        ))}
      </div>
    </div>
  )
}

Exemplo 3: Blog com Comportamento ISR

Se você usava Incremental Static Regeneration (ISR) no Next.js antigo, vai gostar de ver como é simples replicar o mesmo comportamento com o novo sistema.

// app/blog/[slug]/page.tsx
import { cacheLife, cacheTag } from 'next/cache'
import { notFound } from 'next/navigation'

interface Post {
  titulo: string
  conteudo: string
  autor: { nome: string; avatar: string }
  dataPublicacao: string
  tags: string[]
}

interface PageProps {
  params: Promise<{ slug: string }>
}

export default async function BlogPostPage({ params }: PageProps) {
  "use cache"

  const { slug } = await params

  cacheLife('hours')
  cacheTag(`post-${slug}`, 'blog')

  const post: Post | null = await fetch(
    `https://api.blog.com/posts/${slug}`
  ).then(r => {
    if (!r.ok) return null
    return r.json()
  })

  if (!post) {
    notFound()
  }

  return (
    <article>
      <header>
        <h1>{post.titulo}</h1>
        <div className="meta">
          <img src={post.autor.avatar} alt={post.autor.nome} />
          <span>{post.autor.nome}</span>
          <time>{new Date(post.dataPublicacao).toLocaleDateString('pt-BR')}</time>
        </div>
        <div className="tags">
          {post.tags.map(tag => (
            <span key={tag} className="tag">{tag}</span>
          ))}
        </div>
      </header>
      <div
        className="conteudo-post"
        dangerouslySetInnerHTML={{ __html: post.conteudo }}
      />
    </article>
  )
}
// app/api/cms-webhook/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const segredo = request.headers.get('x-webhook-secret')

  if (segredo !== process.env.WEBHOOK_SECRET) {
    return NextResponse.json({ erro: 'Não autorizado' }, { status: 401 })
  }

  const { tipo, dados } = await request.json()

  switch (tipo) {
    case 'post.publicado':
    case 'post.atualizado':
      revalidateTag(`post-${dados.slug}`)
      break
    case 'post.excluido':
      revalidateTag(`post-${dados.slug}`)
      revalidateTag('blog-lista')
      break
    case 'categoria.atualizada':
      revalidateTag('blog')
      break
  }

  return NextResponse.json({ sucesso: true })
}

Esse padrão replica o ISR de forma bem mais flexível. A página é gerada e cacheada na primeira visita, revalidada em segundo plano conforme o cacheLife definido, e pode ser invalidada na hora via webhook quando o conteúdo muda no CMS. Simples e eficaz.

Migração do Next.js 15 para o Next.js 16

Se você tem uma aplicação em Next.js 15, a migração pro novo sistema de cache não é tão assustadora quanto parece. Mas precisa de atenção em alguns pontos. Vamos por partes.

Passo 1: Atualizar a Configuração

// ANTES (Next.js 15)
// next.config.ts
const nextConfig = {
  experimental: {
    dynamicIO: true,
    ppr: true,
  },
}

// DEPOIS (Next.js 16)
// next.config.ts
const nextConfig = {
  cacheComponents: true,
  // PPR agora é habilitado automaticamente com cacheComponents
}

Passo 2: Remover Prefixos unstable_

// ANTES (Next.js 15)
import { unstable_cacheLife, unstable_cacheTag } from 'next/cache'

async function MeuComponente() {
  "use cache"
  unstable_cacheLife('hours')
  unstable_cacheTag('minha-tag')
  // ...
}

// DEPOIS (Next.js 16)
import { cacheLife, cacheTag } from 'next/cache'

async function MeuComponente() {
  "use cache"
  cacheLife('hours')
  cacheTag('minha-tag')
  // ...
}

Passo 3: Revisar Páginas que Dependiam de Cache Implícito

Essa é a mudança mais importante (e a que mais pega gente desprevenida). No Next.js 14/15 sem dynamicIO, muitas páginas eram automaticamente estáticas. No Next.js 16, tudo é dinâmico por padrão. Você vai precisar adicionar "use cache" explicitamente nas páginas que devem ser estáticas.

// ANTES (Next.js 14/15) — implicitamente estática
// app/sobre/page.tsx
export default async function SobrePage() {
  const dados = await fetch('https://api.exemplo.com/sobre')
  // Essa página era automaticamente estática porque o fetch era cacheado
  return <div>{/* ... */}</div>
}

// DEPOIS (Next.js 16) — explicitamente cacheada
// app/sobre/page.tsx
"use cache"

import { cacheLife } from 'next/cache'

export default async function SobrePage() {
  cacheLife('days')

  const dados = await fetch('https://api.exemplo.com/sobre').then(r => r.json())
  return <div>{/* ... */}</div>
}

Passo 4: Substituir revalidate por cacheLife

// ANTES (Next.js 14/15)
// app/blog/page.tsx
export const revalidate = 3600 // revalida a cada 1 hora

export default async function BlogPage() {
  // ...
}

// DEPOIS (Next.js 16)
// app/blog/page.tsx
"use cache"

import { cacheLife } from 'next/cache'

export default async function BlogPage() {
  cacheLife('hours')
  // ...
}

Passo 5: Migrar generateStaticParams

O generateStaticParams continua funcionando normalmente, mas agora trabalha em conjunto com "use cache" pra pré-gerar páginas no build.

// app/produto/[slug]/page.tsx
"use cache"

import { cacheLife, cacheTag } from 'next/cache'

export async function generateStaticParams() {
  const produtos = await fetch('https://api.loja.com/produtos').then(r => r.json())
  return produtos.map((p: { slug: string }) => ({ slug: p.slug }))
}

interface PageProps {
  params: Promise<{ slug: string }>
}

export default async function ProdutoPage({ params }: PageProps) {
  const { slug } = await params

  cacheLife('days')
  cacheTag(`produto-${slug}`)

  const produto = await fetch(`https://api.loja.com/produtos/${slug}`).then(r => r.json())

  return (
    <div>
      <h1>{produto.nome}</h1>
      {/* ... */}
    </div>
  )
}

Boas Práticas e Armadilhas Comuns

Boas Práticas

  • Comece dinâmico, otimize depois: Não tente cachear tudo desde o início. Desenvolva a página como dinâmica, identifique os gargalos com ferramentas de profiling e só então adicione "use cache" onde faz diferença. Essa abordagem evita muita dor de cabeça.
  • Use tags descritivas e hierárquicas: Crie uma convenção de nomenclatura. Por exemplo: produto-123 pra um produto específico, produtos pra invalidar todos, loja pra invalidar toda a seção.
  • Combine cacheLife com cacheTag: Use cacheLife pra definir um TTL base e cacheTag pra permitir invalidação sob demanda. Assim, mesmo que o webhook falhe, o conteúdo será eventualmente atualizado pelo TTL. Cinto e suspensórios.
  • Prefira cache no nível de função pra dados reutilizados: Se a mesma busca de dados acontece em vários componentes, crie uma função cacheada em um módulo separado em vez de cachear cada componente individualmente.
  • Use updateTag apenas em Server Actions: Pra invalidação via webhooks ou rotas de API, use revalidateTag. Reserve updateTag pra ações do usuário onde o feedback imediato importa.
  • Projete fallbacks de Suspense com cuidado: Os fallbacks são a primeira coisa que o usuário vê no PPR. Invista tempo criando skeletons que reflitam o layout final — evita aqueles layout shifts irritantes.

Armadilhas Comuns

  • Esquecer de habilitar cacheComponents: true: A diretiva "use cache" é silenciosamente ignorada sem essa configuração. Sempre verifique o next.config.ts. Sério, esse erro é mais comum do que você imagina.
  • Cachear componentes que usam cookies() ou headers(): Funções dinâmicas como essas são incompatíveis com "use cache". Se precisar de dados do cookie em um componente cacheado, passe-os como props de um componente pai dinâmico.
  • Usar "use cache" em componentes de cliente: A diretiva funciona apenas em Server Components. Pra cachear dados de um Client Component, crie um Server Component wrapper que faz o cache e passa os dados como props.
  • Não envolver componentes dinâmicos em Suspense: Pra que o PPR funcione, cada componente dinâmico dentro de uma página parcialmente estática precisa estar em um <Suspense> boundary com fallback. Sem isso, a página inteira vira dinâmica.
  • Invalidar tags demais: Chamar revalidateTag com uma tag genérica demais (tipo 'todos-os-dados') pode causar uma cascata de revalidações que sobrecarrega o servidor. Use tags específicas sempre que possível.
  • Esquecer que o cache é por argumento: Quando você usa "use cache" em uma função que recebe argumentos, o cache é indexado por eles. Componentes com muitas variações de props podem acabar com um cache bem grande.

Dica: Utilize os cabeçalhos de resposta do Next.js pra debugar o cache em produção. O header x-nextjs-cache indica se a resposta veio do cache (HIT), foi gerada (MISS) ou está sendo revalidada (STALE). Isso economiza muito tempo de investigação.

Comparação de Performance e Quando Usar Cada Estratégia

Pra facilitar a decisão de qual caminho seguir em cada cenário, vamos comparar as abordagens disponíveis.

Totalmente Dinâmico (Sem Cache)

  • TTFB: Mais alto (depende do tempo de processamento do servidor)
  • Quando usar: Páginas totalmente personalizadas, dados em tempo real, operações que dependem de cookies/headers
  • Exemplo: Página de checkout, painel administrativo com dados ao vivo

Cache no Nível de Página ("use cache" no topo)

  • TTFB: Mais baixo (servido direto do cache)
  • Quando usar: Páginas com conteúdo idêntico pra todos os usuários
  • Exemplo: Página institucional, documentação, blog posts

Cache no Nível de Componente ("use cache" dentro do componente)

  • TTFB: Variável (depende da proporção estático/dinâmico)
  • Quando usar: Páginas que misturam conteúdo compartilhado e personalizado
  • Exemplo: Página de produto com preço dinâmico, feed com sidebar cacheada

PPR (Partial Prerendering)

  • TTFB: Extremamente baixo (casca estática servida instantaneamente)
  • Quando usar: Páginas com seções claramente estáticas e dinâmicas, onde velocidade de carregamento inicial é crítica
  • Exemplo: E-commerce, dashboards, redes sociais

Cenários Comuns e Estratégias Recomendadas

Pra deixar mais prático, aqui vai um resumo de cenários do dia a dia:

  • Landing page de marketing: Cache de página com cacheLife('max'). Conteúdo muda só em deploys.
  • Blog com CMS: Cache de página com cacheLife('hours') + cacheTag pra invalidação via webhook.
  • Catálogo de produtos: PPR com informações cacheadas e preço/estoque dinâmicos.
  • Dashboard analytics: PPR com menu e metas cacheados; gráficos e métricas dinâmicos via streaming.
  • Rede social (feed): Dinâmico com componentes auxiliares cacheados (sidebar de tendências, sugestões de perfis).
  • Checkout/pagamento: Totalmente dinâmico. Nunca cachear dados sensíveis ou transacionais.

Medindo o Impacto

Pra medir o impacto real do cache na sua aplicação, essas ferramentas ajudam bastante:

  1. Next.js Analytics: Integrado ao Vercel, mostra métricas de Web Vitals por rota.
  2. Chrome DevTools (aba Network): Observe o TTFB de cada requisição e os headers de cache.
  3. Lighthouse: Execute auditorias regulares pra acompanhar LCP, FID e CLS.
  4. Logging customizado: Adicione logs nos componentes pra verificar quando estão sendo executados vs. servidos do cache.
// Exemplo de logging pra debug de cache
import { cacheLife, cacheTag } from 'next/cache'

export default async function ComponenteComLog({ id }: { id: string }) {
  "use cache"
  cacheLife('hours')
  cacheTag(`componente-${id}`)

  console.log(`[CACHE MISS] ComponenteComLog renderizado para id: ${id} em ${new Date().toISOString()}`)

  const dados = await fetch(`https://api.exemplo.com/dados/${id}`).then(r => r.json())

  return <div>{/* ... */}</div>
}

Padrões Avançados

Composição de Cache em Múltiplos Níveis

Uma das grandes vantagens do novo sistema é poder compor cache em múltiplos níveis. Cada camada tem sua própria política, criando um sistema eficiente e resiliente. Isso é particularmente útil em apps maiores.

// lib/dados-produto.ts
import { cacheLife, cacheTag } from 'next/cache'

export async function buscarProduto(slug: string) {
  "use cache"
  cacheLife('days')
  cacheTag(`produto-dados-${slug}`)

  return fetch(`https://api.loja.com/produtos/${slug}`).then(r => r.json())
}

export async function buscarAvaliacoes(produtoId: string) {
  "use cache"
  cacheLife('hours')
  cacheTag(`avaliacoes-${produtoId}`)

  return fetch(`https://api.loja.com/avaliacoes/${produtoId}`).then(r => r.json())
}
// components/CardProduto.tsx
import { cacheLife, cacheTag } from 'next/cache'
import { buscarProduto, buscarAvaliacoes } from '@/lib/dados-produto'

export default async function CardProduto({ slug }: { slug: string }) {
  "use cache"
  cacheLife('hours')
  cacheTag(`card-produto-${slug}`)

  const produto = await buscarProduto(slug)
  const avaliacoes = await buscarAvaliacoes(produto.id)

  return (
    <div className="card-produto">
      <img src={produto.imagem} alt={produto.nome} />
      <h3>{produto.nome}</h3>
      <p>R$ {produto.preco.toFixed(2)}</p>
      <span>{avaliacoes.media.toFixed(1)} ({avaliacoes.total} avaliações)</span>
    </div>
  )
}

Nessa composição, mesmo que o cache do CardProduto expire, as funções buscarProduto e buscarAvaliacoes podem continuar cacheadas, tornando a re-renderização muito mais rápida. É cache em camadas funcionando a seu favor.

Cache com Parâmetros de Busca

Quando sua página depende de query parameters, é preciso ter um cuidado especial.

// app/busca/page.tsx
import { Suspense } from 'react'
import ResultadosBusca from '@/components/ResultadosBusca'
import FiltrosPopulares from '@/components/FiltrosPopulares'

interface PageProps {
  searchParams: Promise<{ q?: string; pagina?: string; categoria?: string }>
}

export default async function BuscaPage({ searchParams }: PageProps) {
  const params = await searchParams

  return (
    <main>
      <FiltrosPopulares />

      <Suspense
        key={`${params.q}-${params.pagina}-${params.categoria}`}
        fallback={<div>Buscando resultados...</div>}
      >
        <ResultadosBusca
          termo={params.q || ''}
          pagina={parseInt(params.pagina || '1')}
          categoria={params.categoria}
        />
      </Suspense>
    </main>
  )
}
// components/FiltrosPopulares.tsx
import { cacheLife, cacheTag } from 'next/cache'

export default async function FiltrosPopulares() {
  "use cache"
  cacheLife('hours')
  cacheTag('filtros-populares')

  const filtros = await fetch('https://api.loja.com/filtros/populares').then(r => r.json())

  return (
    <aside>
      <h2>Filtros Populares</h2>
      {filtros.map((f: { id: string; nome: string; contagem: number }) => (
        <a key={f.id} href={`/busca?categoria=${f.id}`}>
          {f.nome} ({f.contagem})
        </a>
      ))}
    </aside>
  )
}

Observe o uso da prop key no Suspense. Isso força o React a re-renderizar quando os parâmetros mudam, garantindo que o usuário sempre veja resultados atualizados. Um detalhe pequeno, mas que faz toda a diferença.

Considerações sobre Ambiente de Desenvolvimento

Uma coisa que vale destacar: o comportamento do cache em desenvolvimento é bem diferente da produção. Em next dev, o cache é desabilitado por padrão pra facilitar o desenvolvimento. Isso significa que todos os seus componentes com "use cache" são executados a cada requisição.

Pra testar o comportamento real do cache, a única forma confiável é rodar next build seguido de next start. Não pule essa etapa antes de fazer deploy.

# Testando o cache em modo produção
npm run build && npm run start

Conclusão

O Next.js 16 representa uma mudança significativa na forma como pensamos sobre cache em aplicações React. A transição de cache implícito pra explícito pode parecer mais trabalhosa no início, mas os benefícios em previsibilidade, controle e facilidade de debug compensam muito.

Vamos recapitular os pontos principais:

  • A diretiva "use cache" é o mecanismo central pra opt-in de cache, aplicável em três níveis: arquivo, componente e função.
  • A configuração cacheComponents: true no next.config.ts habilita todo o sistema, substituindo o antigo experimental: { dynamicIO: true }.
  • cacheLife oferece presets práticos e suporte a configurações customizadas pra controle preciso da duração.
  • cacheTag, revalidateTag e updateTag formam um sistema robusto de invalidação sob demanda, com updateTag trazendo semântica de read-your-own-writes em Server Actions.
  • O Partial Prerendering (PPR) combina o melhor dos mundos estático e dinâmico, servindo uma casca HTML instantânea enquanto partes dinâmicas chegam via streaming.
  • A migração do Next.js 15 é direta — basicamente remover prefixos unstable_ e trocar a configuração experimental.

Minha recomendação? Comece com tudo dinâmico e adicione cache onde faz sentido. Use profiling pra encontrar gargalos reais, aplique "use cache" de forma estratégica e sempre teste em modo de produção antes do deploy.

Com essas ferramentas e o conhecimento deste guia, você está pronto pra construir aplicações Next.js 16 que são rápidas, personalizadas e fáceis de manter. O futuro do cache no React é explícito, granular e composável — e já está disponível pra você usar.

Sobre o Autor Marcus Okonkwo

Marcus picked up React in 2017 and has been writing it professionally ever since. Five years at Stripe on the Dashboard team, where he led the migration of the billing UI off a legacy webpack 4 setup onto Next.js with module federation for the embedded merchant components. He shipped the first internal RSC prototype there in late 2023, which mostly worked. Now he's a staff engineer at a logistics startup in Austin, owning the customer-facing Next.js app and the design system that backs it. He spends a disproportionate amount of time thinking about route group conventions, parallel routes for modal UX, and why next/image keeps surprising people on Cloudflare's image resizing. His writing favors annotated code over prose. Seven years of React, two of those deep in App Router land.