LGMdevs
Voltar ao blog
Equipe LGMdevs

Cache no Next.js 16: o modelo mudou, e para melhor

Cache Components e a diretiva use cache trocam o cache implícito e cheio de pegadinhas por um modelo explícito. O que muda na prática e como não se queimar.

Next.jsPerformanceArquitetura

Se você carrega cicatrizes do cache do Next.js — um fetch que ficou preso em dados velhos, uma página que virou dinâmica sem ninguém pedir — o Next.js 16 reescreve essa parte da sua memória. O cache deixou de ser um comportamento implícito que você tentava adivinhar e virou algo que você declara. A peça central chama-se Cache Components, e a porta de entrada é a diretiva use cache.

O problema que existia

No modelo antigo, o cache acontecia por padrão em vários pontos e você passava o dia tentando desligá-lo na hora certa. Um fetch era cacheado a menos que você lembrasse de dizer o contrário; ler um cookie tornava a rota inteira dinâmica. O resultado era um modelo mental difícil: para saber se algo estava em cache, você rastreava regras implícitas espalhadas pela árvore de componentes.

O Next 16 inverte a lógica: nada de dinâmico entra em cache até você marcar. Você liga o recurso no config e passa a declarar, ponto a ponto, o que é cacheável.

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

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

use cache: cache que você declara

Com Cache Components ligado, a diretiva use cache marca uma função, um componente ou uma rota inteira como cacheável. Os argumentos e valores capturados do escopo entram automaticamente na chave do cache — então entradas diferentes geram cache separado, sem você montar a chave na mão.

import { cacheLife } from "next/cache"

// Nível de dado: uma função de busca cacheada
export async function getProdutos() {
  "use cache"
  cacheLife("hours")
  return db.query("SELECT * FROM produtos")
}

O mesmo vale para um componente inteiro (nível de UI): coloque "use cache" no topo do corpo e o HTML renderizado passa a fazer parte do shell estático da página.

A regra de ouro: dado de request fica fora do cache

Aqui mora a pegadinha que evita a maioria dos bugs. Uma função com use cache não pode acessar APIs de request — cookies(), headers(), searchParams. Faz sentido: cache é conteúdo compartilhado entre visitantes; dado de request é específico de um deles.

O padrão correto é ler o valor fora do cache e passá-lo como argumento:

import { cookies } from "next/headers"

// Lê o cookie fora do escopo cacheado...
async function Perfil() {
  const sessao = (await cookies()).get("session")?.value
  return <ConteudoCacheado sessionId={sessao} />
}

// ...e recebe só o valor. sessionId entra na chave do cache.
async function ConteudoCacheado({ sessionId }: { sessionId: string }) {
  "use cache"
  const dados = await buscarDados(sessionId)
  return <div>{dados}</div>
}

Chamar cookies() dentro de use cache falha na hora. Pior é passar uma Promise de dado dinâmico para dentro do escopo cacheado por props ou closure: o build trava esperando um dado que nunca resolve e estoura um timeout depois de 50 segundos. A mensagem é clara quando acontece — mas o hábito de passar valores, não promessas, evita o problema.

O que fica dinâmico: Suspense e streaming

E o que precisa ser fresco a cada request? Não use use cache. Envolva em <Suspense>: o fallback entra no shell estático e o conteúdo real chega via streaming no momento do request.

import { Suspense } from "react"
import { cookies } from "next/headers"

async function Preferencias() {
  const tema = (await cookies()).get("theme")?.value ?? "light"
  return <aside>Seu tema: {tema}</aside>
}

export default function Pagina() {
  return (
    <>
      <h1>Painel</h1>
      <Suspense fallback={<p>Carregando…</p>}>
        <Preferencias />
      </Suspense>
    </>
  )
}

Essa combinação — parte estática, parte cacheada, parte transmitida em streaming — é o Partial Prerendering (PPR), agora o comportamento padrão com Cache Components. E o Next é rígido a favor da sua sanidade: se um componente acessa dado não cacheado e não está nem em use cache nem em <Suspense>, você recebe um erro no build em vez de um bug silencioso em produção.

O cuidado necessário: serverless não guarda memória

Um detalhe que derruba gente experiente. Por padrão, use cache guarda as entradas em memória. Em ambiente serverless (Vercel e afins), cada request pode cair numa instância diferente, então a entrada em memória frequentemente não sobrevive de um request para o outro — a função cacheada re-executa. O cache de build funciona normalmente; o de runtime é que não persiste.

Se você precisa de cache de runtime durável e compartilhado, é aí que entra use cache: remote, que permite um handler dedicado (Redis, KV) — ao custo de um round-trip de rede e, geralmente, taxa da plataforma. Em servidor próprio (self-hosted), a memória persiste entre requests e o problema não existe.

Para invalidar sob demanda, marque a entrada com cacheTag e dispare updateTag (ou revalidateTag) numa Server Action após a mutação:

import { cacheTag } from "next/cache"

async function getPosts() {
  "use cache"
  cacheTag("posts")
  return fetch("https://api.exemplo.com/posts")
}

// Na Server Action, depois de gravar:
// updateTag("posts")  → a próxima visita já vê o dado novo

O resumo prático

O modelo novo troca "adivinhe o que está em cache" por três decisões explícitas para cada pedaço de página: estático (renderiza no build), cacheado (use cache, entra no shell) ou dinâmico (<Suspense>, streaming no request). É mais para declarar — e muito menos para depurar.

Como qualquer major do Next, isto muda entre versões: a fonte da verdade é a documentação da versão que você tem instalada, não a memória de um projeto antigo.


Migrando um projeto para o Next 16 ou decidindo a estratégia de cache do seu produto? Fale com a gente.

Fontes

(Fatos conferidos nos docs da versão instalada, next@16.2.10.)