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
cacheLifeecacheTagficam 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):
- stale — período em que o conteúdo é servido diretamente do cache, sem nenhuma verificação.
- revalidate — intervalo em que o Next.js revalida o cache em segundo plano, servindo o conteúdo antigo enquanto gera o novo.
- 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:
revalidateTagmarca o cache como "stale" e revalida em segundo plano (SWR). Já oupdateTagexpira o cache imediatamente e garante que a próxima leitura retorne dados frescos. UseupdateTagquando 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
- Build time: O Next.js renderiza a casca estática com os componentes cacheados e os fallbacks dos Suspense boundaries.
- Requisição do usuário: O servidor envia imediatamente o HTML estático (TTFB extremamente baixo).
- Streaming: O servidor processa os componentes dinâmicos e transmite o HTML via streaming.
- 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-123pra um produto específico,produtospra invalidar todos,lojapra invalidar toda a seção. - Combine
cacheLifecomcacheTag: UsecacheLifepra definir um TTL base ecacheTagpra 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
updateTagapenas em Server Actions: Pra invalidação via webhooks ou rotas de API, userevalidateTag. ReserveupdateTagpra 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 onext.config.ts. Sério, esse erro é mais comum do que você imagina. - Cachear componentes que usam
cookies()ouheaders(): 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
revalidateTagcom 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-cacheindica 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')+cacheTagpra 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:
- Next.js Analytics: Integrado ao Vercel, mostra métricas de Web Vitals por rota.
- Chrome DevTools (aba Network): Observe o TTFB de cada requisição e os headers de cache.
- Lighthouse: Execute auditorias regulares pra acompanhar LCP, FID e CLS.
- 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: truenonext.config.tshabilita todo o sistema, substituindo o antigoexperimental: { dynamicIO: true }. cacheLifeoferece presets práticos e suporte a configurações customizadas pra controle preciso da duração.cacheTag,revalidateTageupdateTagformam um sistema robusto de invalidação sob demanda, comupdateTagtrazendo 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.