Next.js 16 cacheComponents: Migrando 91.000 Páginas do App Router Caching
Estávamos executando um grande catálogo de e-commerce no App Router do Next.js 14 há cerca de dezoito meses quando o Next.js 16 foi lançado. 91.247 páginas. Listagens de produtos, árvores de categorias, conteúdo editorial, variantes localizadas em 14 mercados. O antigo modelo de cache — onde Server Components eram armazenados em cache por padrão — havia se tornado um campo minado de bugs de dados obsoletos e spaghetti de revalidateTag. Quando a equipe do Next.js anunciou cacheComponents e a mudança para sem-cache-por-padrão no Next.js 15 (continuada e refinada na v16), sabíamos que era hora. Esta é a história dessa migração: o que funcionou, o que não funcionou e os números de desempenho do outro lado.
Sumário
- O Problema de Cache que Realmente Tínhamos
- O que Mudou no Next.js 15 e 16
- Entendendo cacheComponents
- Nossa Estratégia de Migração para 91.000 Páginas
- Implementação: Passo a Passo
- Resultados de Desempenho e Benchmarks
- Armadilhas e Pegadinhas
- Quando Você Deve e Não Deve Usar cacheComponents
- FAQ

O Problema de Cache que Realmente Tínhamos
Deixe-me pintar o quadro. No App Router do Next.js 14, requisições fetch em Server Components eram armazenadas em cache por padrão. O Data Cache persistia entre implementações. O Full Route Cache armazenava HTML renderizado e cargas RSC no momento da construção. E o Router Cache no lado do cliente mantinha segmentos pré-carregados por... bem, mais tempo do que você esperaria.
Para um site com 91.000 páginas, essa abordagem padrão-cache-tudo criou duas categorias de problemas:
Dados obsoletos em todo lugar. Preços de produtos atualizados em nosso CMS headless (Sanity, no nosso caso) mas os resultados de fetch armazenados em cache permaneciam. Tínhamos chamadas de revalidateTag espalhadas por 47 ações de servidor diferentes. Perder uma tag? Um cliente vê o preço de ontem. Literalmente tínhamos um canal Slack chamado #cache-crimes onde a equipe de conteúdo reportava páginas obsoletas.
Tempos de construção do inferno. A geração estática completa de 91.000 páginas levava mais de 3 horas. Tínhamos nos movido para ISR com revalidate: 3600 para a maioria das páginas, mas a interação entre ISR, o Data Cache e revalidação sob demanda era genuinamente difícil de raciocinar. Novos desenvolvedores no time gastariam suas primeiras duas semanas apenas entendendo as camadas de cache.
O Custo do Modelo Mental
Aqui está o que acho que as pessoas subestimam: o custo cognitivo do cache implícito. Quando o cache é o padrão e você opta por sair dele, cada novo componente requer que você pergunte "deve estar em cache?" e então lembre-se de adicionar a diretiva correta se a resposta for não. Quando sem-cache é o padrão e você opta por entrar nele, você só pensa sobre cache quando realmente o quer. Este é um modelo fundamentalmente diferente -- e melhor.
O que Mudou no Next.js 15 e 16
Next.js 15 foi a grande mudança filosófica. A equipe inverteu os padrões:
| Comportamento | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
fetch() em Server Components |
Cache por padrão | Sem cache por padrão | Sem cache por padrão |
| Route Handlers (GET) | Cache por padrão | Sem cache por padrão | Sem cache por padrão |
| Client Router Cache | 30s (dinâmico) / 5min (estático) | 0s para segmentos de página | 0s padrão, configurável |
| Full Route Cache | Ativado para rotas estáticas | Mesmo | Mesmo, com refinamentos de cacheLife |
| Cache em nível de componente | unstable_cache |
Diretiva use cache (experimental) |
API cacheComponents (estável) |
Next.js 15 introduziu a diretiva use cache como um recurso experimental atrás de um flag. Next.js 16, lançado no início de 2025, estabilizou isso como a opção de configuração cacheComponents e a diretiva "use cache" associada, juntamente com cacheLife para definir perfis de cache personalizados e cacheTag para invalidação direcionada.
O insight chave: o cache passou de ser um comportamento de framework implícito para uma escolha explícita do desenvolvedor em nível de componente. Isto é enorme para sites grandes.
Entendendo cacheComponents
O recurso cacheComponents em next.config.js ativa cache em nível de componente através da diretiva "use cache". Aqui está a configuração básica:
// next.config.js (Next.js 16)
const nextConfig = {
experimental: {
cacheComponents: true,
},
};
module.exports = nextConfig;
Uma vez ativado, você pode adicionar "use cache" no topo de qualquer Server Component assíncrono, ação de servidor ou até um arquivo de layout:
// app/products/[slug]/page.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export default async function ProductPage({ params }: { params: { slug: string } }) {
cacheLife('products'); // perfil de cache personalizado
cacheTag(`product-${params.slug}`);
const product = await fetchProduct(params.slug);
return (
<div>
<h1>{product.name}</h1>
<ProductDetails product={product} />
<DynamicPricing productId={product.id} /> {/* Este componente NÃO está em cache */}
</div>
);
}
Perfis de cacheLife
É aqui que fica interessante para sites grandes. Você define perfis de cache nomeados em next.config.js:
const nextConfig = {
experimental: {
cacheComponents: true,
cacheLife: {
products: {
stale: 300, // servir obsoleto por 5 minutos
revalidate: 3600, // revalidar após 1 hora
expire: 86400, // expiração forçada após 24 horas
},
editorial: {
stale: 3600,
revalidate: 86400,
expire: 604800, // 7 dias
},
navigation: {
stale: 86400,
revalidate: 604800,
expire: 2592000, // 30 dias
},
},
},
};
O modelo de três camadas (stale, revalidate, expire) mapeia bem para semântica de stale-while-revalidate. Durante a janela de stale, o conteúdo em cache é servido imediatamente. Depois de stale mas antes de expire, uma revalidação em background é acionada. Após expire, a entrada de cache se foi.
cacheTag para Invalidação
A função cacheTag substitui o antigo padrão de revalidateTag por algo mais componível:
import { revalidateTag } from 'next/cache';
// Em um manipulador de webhook ou ação de servidor:
export async function handleProductUpdate(productSlug: string) {
revalidateTag(`product-${productSlug}`);
revalidateTag('product-listing'); // invalidar páginas de listagem também
}
Esta parte não mudou muito do Next.js 15, mas funciona muito melhor com cacheComponents porque você está marcando componentes em cache específicos em vez de tentar invalidar caches de framework opacos.

Nossa Estratégia de Migração para 91.000 Páginas
Não fizemos isso em um único golpe. Com 91.000 páginas em 14 localidades, uma migração tudo-de-uma-vez teria sido imprudente. Veja como dividimos:
Fase 1: Atualizar para Next.js 16, Sem Mudanças de Cache (Semana 1-2)
Atualizamos do Next.js 14.2 para 16.0 sem ativar cacheComponents. Isso sozinho mudou o comportamento porque requisições fetch não estavam mais em cache por padrão. Esperávamos regressões de TTFB e as obtivemos:
- TTFB médio passou de 180ms para 340ms em páginas de produtos
- Carga do servidor de origem aumentou em ~60% (nosso CDN Sanity se comportou bem, mas nossos endpoints de API customizados não)
- Revalidação ISR realmente ficou mais rápida porque havia menos estado de cache para gerenciar
Isso confirmou o que suspeitávamos: tínhamos estado dependendo fortemente de cache implícito, e muitas das nossas páginas genuinamente precisavam de cache -- apenas cache explícito e intencional.
Fase 2: Auditar e Classificar Páginas (Semana 3)
Categorizamos cada rota em nossa aplicação:
| Tipo de Página | Contagem | Estratégia de Cache | Perfil cacheLife |
|---|---|---|---|
| Páginas de detalhe de produto | 42.000 | Cache com tag de produto | products (5min obsoleto / 1hr revalidar) |
| Páginas de listagem de categoria | 3.200 | Cache com tag de categoria | products (5min obsoleto / 1hr revalidar) |
| Páginas editorial/blog | 8.400 | Cache agressivamente | editorial (1hr obsoleto / 24hr revalidar) |
| Variantes localizadas | 31.647 | Mesmo que página base | Herdado da base |
| Páginas de conta/dinâmicas | 6.000 | Sem cache | N/A |
Fase 3: Ativar cacheComponents, Adicionar Diretivas (Semana 4-6)
Ativamos o flag e começamos a adicionar diretivas "use cache". A decisão chave: cacheamos em nível de página para a maioria das rotas, mas em nível de componente para páginas com conteúdo misto estático/dinâmico.
Para páginas de produtos, a informação do produto e imagens foram armazenadas em cache, mas o componente de preço e status de inventário foram deixados sem cache:
// components/ProductInfo.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export async function ProductInfo({ slug }: { slug: string }) {
cacheLife('products');
cacheTag(`product-${slug}`, 'product-info');
const product = await getProduct(slug);
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductImages images={product.images} />
</section>
);
}
// components/DynamicPricing.tsx
// SEM diretiva "use cache" -- sempre fresco
export async function DynamicPricing({ productId }: { productId: string }) {
const pricing = await getPricing(productId); // atinge API de preço a cada requisição
return (
<div className="pricing">
<span className="price">${pricing.current}</span>
{pricing.onSale && <span className="was-price">${pricing.original}</span>}
</div>
);
}
Fase 4: Integração de Webhook (Semana 7)
Reconfiguramos nossos webhooks Sanity para chamar revalidateTag com as tags corretas. Isto foi realmente mais simples do que nossa antiga configuração porque tags eram agora explícitas no código, não espalhadas entre opções de fetch.
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
switch (body._type) {
case 'product':
revalidateTag(`product-${body.slug.current}`);
revalidateTag('product-listing');
break;
case 'category':
revalidateTag(`category-${body.slug.current}`);
revalidateTag('navigation');
break;
case 'article':
revalidateTag(`article-${body.slug.current}`);
break;
}
return new Response('OK', { status: 200 });
}
Implementação: Passo a Passo
Se você está fazendo uma migração similar, aqui está o guia prático que recomendaríamos (e o que agora usamos para projetos de desenvolvimento Next.js na Social Animal):
Passo 1: Ativar o Flag
// next.config.js
module.exports = {
experimental: {
cacheComponents: true,
cacheLife: {
// Começar com padrões sensatos
default: {
stale: 60,
revalidate: 900,
expire: 86400,
},
},
},
};
Passo 2: Encontrar Seus Caminhos Quentes
Use sua análise para identificar as páginas que recebem mais tráfego e onde TTFB importa mais. Para nós, eram páginas de categoria (alto tráfego, conteúdo relativamente estável) e páginas de produtos (alto tráfego, conteúdo moderadamente dinâmico).
Passo 3: Adicionar `"use cache"` de Cima para Baixo
Comece com layouts. Se seu layout raiz busca dados de navegação, cache isso primeiro -- é a mudança de maior impacto e menor risco:
// app/layout.tsx
// Nota: "use cache" em layouts faz cache do shell do layout
// Páginas filhas ainda renderizam independentemente
import { Navigation } from '@/components/Navigation';
export default async function RootLayout({ children }) {
return (
<html>
<body>
<Navigation /> {/* Este componente tem seu próprio "use cache" */}
{children}
</body>
</html>
);
}
Passo 4: Configurar Monitoramento
Usamos análises integradas do Vercel mais logging customizado para rastrear taxas de acerto de cache. Na primeira semana após ativar cacheComponents, nossa taxa de acerto de cache era apenas 34%. Após ajustar durações de stale, ela subiu para 78%.
Resultados de Desempenho e Benchmarks
Aqui estão os números reais após a migração completa, medidos durante um período de 30 dias no plano Pro do Vercel:
| Métrica | Antes (Next.js 14) | Após Fase 1 (v16, sem cache) | Após Migração Completa |
|---|---|---|---|
| TTFB médio (páginas de produtos) | 180ms | 340ms | 95ms |
| TTFB médio (páginas de categoria) | 220ms | 410ms | 72ms |
| TTFB médio (páginas editorial) | 150ms | 280ms | 45ms |
| TTFB P99 (todas as páginas) | 1.200ms | 2.100ms | 380ms |
| Tempo de construção (completo) | 3h 12min | 2h 48min | 48min |
| Invocações de função Vercel/dia | 2,4M | 3,8M | 1,1M |
| Conta mensal Vercel | ~$840 | ~$1.200 | ~$520 |
| Taxa de acerto de cache | Desconhecida (implícita) | N/A | 78% |
| Incidentes de conteúdo obsoleto (#cache-crimes) | 8-12/semana | 0 | 1-2/mês |
A melhoria de tempo de construção merece explicação. Com cacheComponents, nos afastamos de gerar todos os 91.000 páginas no tempo de construção. Em vez disso, estaticamente geramos apenas as top 5.000 páginas (por tráfego) e deixamos o resto gerar sob demanda com cache. A diretiva cacheComponents significava que essas páginas sob demanda ficavam em cache após a primeira visita, com cacheLife controlando obsolescência.
A queda da conta Vercel foi significativa. Menos invocações de função (porque de cache de componente explícito) mais tempos de construção mais curtos significou economia real. Essa redução de ~$320/mês se paga.
Armadilhas e Pegadinhas
Limites de Serialização
A diretiva "use cache" cria um limite de serialização. Tudo passado para um componente em cache como props deve ser serializável. Tínhamos vários componentes que recebiam funções de callback ou elementos React como props -- aqueles quebraram imediatamente. A solução foi reestruturar para usar padrões de composição em vez disso:
// ❌ Isto quebra com "use cache"
"use cache";
export async function ProductCard({ product, onAddToCart }) {
// onAddToCart é uma função -- não serializável!
}
// ✅ Isto funciona
"use cache";
export async function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
{/* AddToCart é um Client Component, não em cache */}
<AddToCartButton productId={product.id} />
</div>
);
}
Params Dinâmicos e Explosão de Chave de Cache
Com 91.000 páginas, cada uma com params únicos, o espaço de chave de cache é enorme. Atingimos limites de cache de borda do Vercel na primeira semana e tivemos que ser mais estratégicos sobre quais páginas receberam longas durações de expire. Páginas de cauda longa com baixo tráfego receberam durações de cache mais curtas.
A Armadilha de `Date.now()`
Qualquer componente usando "use cache" que chama Date.now() ou new Date() dentro da função em cache irá cache aquele timestamp. Encontramos isto em uma exibição "última atualização" que mostrava o mesmo tempo por horas. A solução: mover lógica sensível ao tempo para um Client Component ou um Server Component sem cache.
Limites de Cache Aninhados
Quando você aninha componentes em cache dentro de outros componentes em cache, o cache interno tem seu próprio ciclo de vida. Isto é poderoso mas confuso. Estabelecemos uma convenção de time: cache em nível de página OU em nível de componente, não ambos, a menos que haja uma razão clara.
Quando Você Deve e Não Deve Usar cacheComponents
Use quando:
- Você tem mais de algumas centenas de páginas e tempos de construção ISR são dolorosos
- Seu conteúdo tem requisitos de frescura clara que variam por seção
- Você precisa de controle granular sobre o que está em cache vs. sempre-fresco
- Você está rodando em Vercel ou uma plataforma que suporta as camadas de cache do Next.js
- Você quer reduzir custos de infraestrutura em sites de alto tráfego
Não use quando:
- Seu site é pequeno o suficiente que SSG completo funciona bem
- Cada página é totalmente dinâmica (conteúdo específico do usuário em toda parte)
- Você não está em uma plataforma de hospedagem que suporte infraestrutura de caching do Next.js
- Seu time é novo em Next.js -- se familiarize com o básico primeiro
Se você está avaliando se seu projeto precisa deste nível de controle de cache, ou se um framework diferente como Astro pode ser um ajuste melhor para seu site com muitos conteúdos, vale a pena pensar antes de se comprometer com uma migração.
Para projetos onde conteúdo vem de múltiplas fontes de CMS headless, o sistema cacheTag no Next.js 16 funciona lindamente com arquiteturas de CMS headless -- cada tipo de conteúdo recebe seu próprio canal de invalidação.
FAQ
O que é cacheComponents no Next.js 16?
cacheComponents é uma opção de configuração experimental no Next.js 16 que ativa a diretiva "use cache" para Server Components. Permite que você explicitamente marque quais componentes devem estar em cache e defina perfis de cache personalizados usando cacheLife. É a evolução estável da diretiva use cache que era experimental no Next.js 15.
Como cacheComponents é diferente de ISR (Incremental Static Regeneration)?
ISR faz cache de páginas inteiras e as revalida em um cronograma baseado em tempo. cacheComponents permite que você faça cache de componentes individuais dentro de uma página, cada um com diferentes tempos de vida de cache. Uma única página pode ter um header em cache por 24 horas, info de produto em cache por 1 hora, e preço que nunca está em cache. ISR não consegue fazer isto -- é tudo ou nada em nível de página.
Preciso estar em Vercel para usar cacheComponents?
Não, mas a experiência é melhor em Vercel porque a infraestrutura de cache está bem integrada. Implantações de Next.js auto-hospedadas podem usar cacheComponents com o adaptador de cache do sistema de arquivos, mas você não obterá benefícios de distribuição de edge. Plataformas como Netlify e Cloudflare estão adicionando suporte, mas a partir de meados de 2025, Vercel permanece a implementação mais completa.
Como posso invalidar componentes em cache no Next.js 16?
Você usa cacheTag() dentro de seu componente em cache para atribuir tags, depois chama revalidateTag('nome-da-tag') de uma ação de servidor, manipulador de rota ou endpoint de webhook. Isto invalida todos os componentes em cache com aquela tag. É a mesma API do Next.js 15, mas é mais útil agora porque você está marcando componentes em cache explícitos em vez de caches de framework implícitos.
cacheComponents vai reduzir minha conta Vercel?
Pode reduzir significativamente custos. No nosso caso, invocações de função caíram em 54% porque respostas de componente em cache foram servidas da camada de cache em vez de invocar funções serverless. A redução de tempo de construção também economiza em minutos de construção. Seus resultados variarão dependendo de padrões de tráfego e taxas de acerto de cache -- verifique a calculadora de preços do Vercel com seu uso atual.
O que acontece se eu adicionar "use cache" a um componente que recebe props não-serializáveis?
Você receberá um erro de construção. A diretiva "use cache" cria um limite de serialização, então todos os props devem ser serializáveis (strings, números, objetos simples, arrays). Funções, elementos React, instâncias de classe e outros valores não-serializáveis causarão falha na construção. Reestruture seu componente para aceitar apenas props de dados e lidar com interatividade em Client Components filhos.
Posso usar cacheComponents com React Server Components de outros frameworks?
Não. cacheComponents é um recurso específico do Next.js que se constrói sobre React's Server Components. Enquanto a sintaxe da diretiva "use cache" pode eventualmente se tornar um padrão React, os perfis de cacheLife e sistema de cacheTag são APIs do Next.js. Se você está usando um framework como Remix ou um setup RSC customizado, você precisará de estratégias de cache diferentes.
Quanto tempo leva para migrar um site Next.js grande para cacheComponents?
Para nosso site de 91.000 páginas com um time de 4 desenvolvedores, a migração completa levou 7 semanas incluindo testes e monitoramento. Um site menor (sob 10.000 páginas) com um modelo de dados mais simples provavelmente conseguiria fazer em 1-2 semanas. As mudanças de código real são diretas -- o tempo vai para auditar suas necessidades de cache, testar fluxos de invalidação e monitorar taxas de acerto de cache após implantação. Se você prefere não ir sozinho, entre em contato conosco -- fizemos isto algumas vezes agora.