Seu deploy sai às 2h14 da manhã. Noventa e uma mil páginas de produtos—árvores de categorias, variantes localizadas em 14 mercados, conteúdo SEO editorial—migram do App Router do Next.js 14 para a API cacheComponents da v16. Você observa o primeiro request em cascata nos logs do Vercel. TTFB sobe para 1.8 segundos. Seu Slack vibra. Por dezoito meses você tinha batalhado contra o modelo default-cache antigo: preços antigos, chamadas revalidateTag que disparavam muito tarde, tickets de suporte ao cliente culpando "o website". Next.js 15 desligou o cache por padrão; v16 te deu cacheComponents para optar de volta cirurgicamente. Você escolheu migração sobre morte lenta por mil bugs de cache. Agora você está olhando para tráfego real, erros reais, e um incident em produção duas horas antes do amanhecer. Aqui está o que quebrou, o que resistiu, e os deltas de performance que nenhuma suite de benchmark previu.

Índice

Next.js 16 cacheComponents: Migrando 91.000 Páginas do Cache do App Router

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 cached por padrão. O Data Cache persistia entre deployments. O Full Route Cache armazenava HTML renderizado e RSC payloads no tempo de build. E o Router Cache no lado do cliente mantinha segments pré-carregados por... bem, mais tempo do que você esperaria.

Para um site com 91.000 páginas, essa abordagem default-cache-everything criou duas categorias de problemas:

Dados antigos em todo lugar. Preços de produtos atualizados em nosso headless CMS (Sanity, no nosso caso) mas os resultados fetch em cache ficavam. Tínhamos chamadas revalidateTag espalhadas por 47 server actions diferentes. Perder uma tag? Um cliente vê o preço de ontem. Nós literalmente tínhamos um canal Slack chamado #cache-crimes onde o time de conteúdo reportava páginas antigo.

Tempos de build 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, Data Cache, e revalidação on-demand era genuinamente difícil de raciocinar. Novos desenvolvedores no time gastariam suas primeiras duas semanas só entendendo as camadas de cache.

O Custo do Modelo Mental

Aqui está o que eu acho que as pessoas subestimam: o custo cognitivo do cache implícito. Quando o cache é o padrão e você desativa, cada novo componente exige que você pergunte "isso deve ser cacheado?" e então lembre-se de adicionar a diretiva correta se a resposta é não. Quando sem-cache é o padrão e você ativa, você só pensa em cache quando o quer ativamente. Esse é 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. O time inverteu os padrões:

Comportamento Next.js 14 Next.js 15 Next.js 16
fetch() em Server Components Cached por padrão Não cacheado por padrão Não cacheado por padrão
Route Handlers (GET) Cached por padrão Não cacheado por padrão Não cacheado por padrão
Client Router Cache 30s (dinâmico) / 5min (estático) 0s para page segments 0s por padrão, configurável
Full Route Cache Habilitado para rotas estáticas Mesmo Mesmo, com refinamentos 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 uma flag. Next.js 16, lançado no início de 2025, estabilizou isso como a opção de configuração cacheComponents e a diretiva associada "use cache", junto com cacheLife para definir perfis de cache customizados e cacheTag para invalidação direcionada.

A percepção-chave: cache passou de ser um comportamento implícito do framework para uma escolha explícita do desenvolvedor no nível do componente. Isso é um grande negócio para sites grandes.

Entendendo cacheComponents

O recurso cacheComponents em next.config.js habilita cache no nível do 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 habilitado, você pode adicionar "use cache" no topo de qualquer async Server Component, server action, 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 customizado
  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 é cacheado */}
    </div>
  );
}

Perfis cacheLife

Aqui é onde fica interessante para sites grandes. Você define perfis de cache nomeados em next.config.js:

const nextConfig = {
  experimental: {
    cacheComponents: true,
    cacheLife: {
      products: {
        stale: 300,      // serve stale por 5 minutos
        revalidate: 3600, // revalida após 1 hora
        expire: 86400,    // expira hard 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 stale-while-revalidate. Durante a janela stale, conteúdo cacheado é servido imediatamente. Após stale mas antes de expire, uma revalidação em background inicia. Após expire, a entrada de cache desaparece.

cacheTag para Invalidação

A função cacheTag substitui o padrão antigo revalidateTag com algo mais composável:

import { revalidateTag } from 'next/cache';

// Em um webhook handler ou server action:
export async function handleProductUpdate(productSlug: string) {
  revalidateTag(`product-${productSlug}`);
  revalidateTag('product-listing'); // invalida páginas de listing também
}

Esta parte não mudou muito desde Next.js 15, mas funciona muito melhor com cacheComponents porque você está tagging componentes cacheados específicos ao invés de tentar invalidar caches do framework opacos.

Next.js 16 cacheComponents: Migrando 91.000 Páginas do Cache do App Router - arquitetura

Nossa Estratégia de Migração para 91.000 Páginas

Não fizemos tudo de uma vez. Com 91.000 páginas em 14 locales, uma migração big-bang seria imprudente. Aqui está como dividimos:

Fase 1: Upgrade para Next.js 16, Sem Mudanças de Cache (Semana 1-2)

Atualizamos de Next.js 14.2 para 16.0 sem habilitar cacheComponents. Isso sozinho mudou o comportamento porque requisições fetch não eram mais cacheadas por padrão. Esperávamos regressões de TTFB e as conseguimos:

  • TTFB médio foi de 180ms para 340ms em páginas de produto
  • Carga do servidor de origem aumentou em ~60% (nosso CDN Sanity resistiu bem, mas nossos endpoints de API custom não)
  • Revalidação de ISR ficou realmente mais rápido porque havia menos estado de cache para gerenciar

Isso confirmou o que suspeitávamos: tínhamos estado inclinando-nos pesadamente para cache implícito, e muitas das nossas páginas genuinamente precisavam de cache -- só que cache explícito, intencional.

Fase 2: Auditoria e Classificação de Páginas (Semana 3)

Categorização cada rota no nosso app:

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 stale / 1hr revalidate)
Páginas de listing de categoria 3.200 Cache com tag de categoria products (5min stale / 1hr revalidate)
Páginas editorial/blog 8.400 Cache agressivo editorial (1hr stale / 24hr revalidate)
Variantes localizadas 31.647 Mesmo que página base Herdado da base
Páginas conta/dinâmicas 6.000 Sem cache N/A

Fase 3: Habilitar cacheComponents, Adicionar Diretivas (Semana 4-6)

Habilitamos a flag e começamos a adicionar diretivas "use cache". A decisão-chave: cacheamos no nível da página para a maioria das rotas, mas no nível do componente para páginas com conteúdo misto estático/dinâmico.

Para páginas de produto, as informações do produto e imagens eram cacheadas, mas o componente de pricing e status de inventário não eram:

// 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); // bate na API de pricing a cada request
  
  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)

Reconectamos nossos webhooks Sanity para chamar revalidateTag com as tags corretas. Isso era realmente mais simples que nossa configuração anterior porque tags agora eram explícitas no código, não espalhadas por 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 playbook prático que recomendaríamos (e o que agora usamos para projetos de desenvolvimento Next.js na Social Animal):

Passo 1: Habilitar a Flag

// next.config.js
module.exports = {
  experimental: {
    cacheComponents: true,
    cacheLife: {
      // Comece com padrões sensatos
      default: {
        stale: 60,
        revalidate: 900,
        expire: 86400,
      },
    },
  },
};

Passo 2: Encontre Seus Caminhos Quentes

Use sua analytics 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 produto (alto tráfego, conteúdo moderadamente dinâmico).

Passo 3: Adicione `"use cache"` Top-Down

Comece com layouts. Se seu layout raiz busca dados de navegação, cache isso primeiro -- é o impacto mais alto, risco mais baixo:

// app/layout.tsx
// Note: "use cache" em layouts cachea o layout shell
// 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: Configure Monitoramento

Usamos analytics built-in do Vercel plus logging customizado para rastrear cache hit rates. Na primeira semana depois de habilitar cacheComponents, nossa taxa de cache hit era só 34%. Depois de ajustar durações stale, subiu para 78%.

Resultados de Performance e Benchmarks

Aqui estão os números reais após a migração completa, medidos ao longo de 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 produto) 180ms 340ms 95ms
TTFB médio (páginas de categoria) 220ms 410ms 72ms
TTFB médio (páginas editorial) 150ms 280ms 45ms
P99 TTFB (todas as páginas) 1.200ms 2.100ms 380ms
Tempo de build (completo) 3h 12min 2h 48min 48min
Invocações de função Vercel/dia 2.4M 3.8M 1.1M
Fatura mensal Vercel ~$840 ~$1.200 ~$520
Taxa de cache hit Desconhecido (implícito) N/A 78%
Incidents de conteúdo antigo (#cache-crimes) 8-12/semana 0 1-2/mês

A melhoria no tempo de build merece explicação. Com cacheComponents, nos afastamos de gerar todos os 91.000 páginas no tempo de build. Em vez disso, geramos estaticamente só as top 5.000 páginas (por tráfego) e deixamos o resto gerar on-demand com cache. A diretiva cacheComponents significou que essas páginas on-demand ficaram em cache após primeira visit, com cacheLife controlando staleness.

A queda da fatura Vercel foi significativa. Menos invocações de função (por causa do cache explícito de componentes) plus tempos de build mais curtos significaram economias reais. Essa redução de ~$320/mês se paga sozinha.

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 callback ou React elements como props -- esses quebraram imediatamente. A fix era reestruturar para usar padrões de composição:

// ❌ 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 cacheado */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

Params Dinâmicos e Explosão de Espaço de Chave

Com 91.000 páginas, cada uma com params únicos, o espaço de chave de cache é enorme. Nós batemos nos limites de edge cache do Vercel na primeira semana e tivemos que ser mais estratégicos sobre quais páginas conseguiam longas durações expire. Páginas de cauda longa de baixo tráfego conseguiram durações de cache mais curtas.

A Armadilha `Date.now()`

Qualquer componente usando "use cache" que chame Date.now() ou new Date() dentro da função cacheada vai cachear esse timestamp. Encontramos isto em uma exibição "última atualização" que mostrava a mesma hora por horas. A fix: mover lógica sensível ao tempo para um Client Component ou um Server Component não cacheado.

Limites de Cache Aninhados

Quando você aninha componentes cacheados dentro de outros componentes cacheados, o cache interno tem seu próprio ciclo de vida. Isso é poderoso mas confuso. Estabelecemos uma convenção de time: cache no nível da página OU no nível do componente, não ambos, a menos que haja uma razão clara.

Quando Usar e Não Usar cacheComponents

Use quando:

  • Você tem mais que algumas centenas de páginas e tempos de build ISR são dolorosos
  • Seu conteúdo tem requisitos de freshness claros que variam por seção
  • Você precisa de controle granular sobre o que é cacheado vs. sempre-fresco
  • Você está rodando no Vercel ou uma plataforma que suporta as camadas de cache Next.js
  • Você quer reduzir custos de infraestrutura em sites de alto tráfego

Não use quando:

  • Seu site é pequeno o bastante que SSG completo funciona bem
  • Cada página é totalmente dinâmica (conteúdo específico do usuário em todo lugar)
  • Você não está em uma plataforma de hosting que suporte infraestrutura de cache Next.js
  • Seu time é novo em Next.js -- fique confortável 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 poderia ser melhor para seu site focado em conteúdo, isso vale a pena pensar antes de se comprometer com uma migração.

Para projetos onde conteúdo vem de múltiplas fontes de headless CMS, o sistema cacheTag em Next.js 16 funciona lindamente com arquiteturas de headless CMS -- 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 habilita a diretiva "use cache" para Server Components. Deixa você marcar explicitamente quais componentes devem ser cacheados e definir perfis de cache customizados 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 cachea páginas inteiras e revalida-as em um schedule baseado em tempo. cacheComponents deixa você cachear componentes individuais dentro de uma página, cada um com diferentes lifetimes de cache. Uma única página pode ter um header cacheado por 24 horas, info de produto cacheada por 1 hora, e pricing que nunca é cacheado. ISR não pode fazer isso -- é tudo ou nada no nível da página.

Eu preciso estar no Vercel para usar cacheComponents?

Não, mas a experiência é melhor no Vercel porque a infraestrutura de cache é tightly integrada. Deployments self-hosted Next.js podem usar cacheComponents com o adaptador de cache do file system, mas você não vai conseguir os benefícios de distribuição de edge. Plataformas como Netlify e Cloudflare estão adicionando suporte, mas a partir de mid-2026, Vercel permanece a implementação mais completa.

Como eu invalido componentes em cache no Next.js 16?

Você usa cacheTag() dentro de seu componente cacheado para atribuir tags, e então chama revalidateTag('tag-name') de um server action, route handler, ou endpoint de webhook. Isso invalida todos os componentes cacheados com essa tag. É a mesma API do Next.js 15, mas é mais útil agora porque você está tagging componentes cacheados explícitos ao invés de caches opacos do framework.

cacheComponents vai reduzir minha fatura Vercel?

Pode reduzir custos significativamente. No nosso caso, invocações de função caíram 54% porque respostas de componentes cacheados eram servidas da camada de cache ao invés de invocar funções serverless. A redução no tempo de build também economiza em minutos de build. Seu resultado vai variar baseado em padrões de tráfego e taxas de cache hit -- verifique a calculadora de preços Vercel com seu uso atual.

O que acontece se eu adicionar "use cache" a um componente que recebe props não-serializáveis?

Você vai conseguir um erro de build. A diretiva "use cache" cria um limite de serialização, então todas as props devem ser serializáveis (strings, números, objetos planos, arrays). Funções, React elements, instâncias de classe, e outros valores não-serializáveis vão causar o build a falhar. Reestruture seu componente para aceitar só props de dados e lidar com interatividade em Client Components filhos.

Posso usar cacheComponents com React Server Components de outros frameworks?

Não. cacheComponents é uma feature Next.js-específica que se constrói em cima de React's Server Components. Enquanto a sintaxe da diretiva "use cache" pode eventualmente se tornar um padrão React, os perfis cacheLife e o sistema cacheTag são APIs Next.js. Se você está usando um framework como Remix ou um setup RSC customizado, você vai 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 poderia provavelmente fazer em 1-2 semanas. As mudanças de código atual são simples -- o tempo vai em auditar suas necessidades de cache, testar fluxos de invalidação, e monitorar taxas de cache hit após deployment. Se você preferir não ir sozinho, entre em contato conosco -- já fizemos isso algumas vezes agora.