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.

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 Editorial Team

Our team of expert writers and editors.