Artigo

bf-cache: como evitar que seu site seja marcado como não cacheável

Entenda o impacto do bf-cache no desempenho do seu site, por que páginas podem ser marcadas como não cacheáveis e como corrigir esse problema no Next.js.


Rodei o PageSpeed Insights no meu blog outro dia e me deparei com um aviso que eu nunca tinha visto antes:

Page prevented back/forward cache restoration — 3 failure reasons

Marcado como "fora da pontuação" — ou seja, não derruba sua nota, o que talvez seja o motivo de tanta gente ignorar. Mas ignorar isso tem um custo real: toda vez que alguém aperta "voltar" no navegador pra chegar na sua página, em vez de uma restauração instantânea (zero requisições, estado JS preservado), o navegador refaz o carregamento do zero. Esse texto é sobre como eu cacei a causa disso — e sobre como minha primeira teoria estava errada.

O que é bfcache, rapidinho

O back/forward cache é um mecanismo que praticamente todo navegador moderno tem: quando você navega pra fora de uma página, em vez de descartá-la, o navegador congela ela inteira (DOM, estado de JS, tudo) numa memória temporária. Se você aperta "voltar", ele simplesmente descongela — sem re-requisitar nada, sem re-executar JS do zero. Instantâneo.

O problema: existe uma lista de coisas que desqualificam uma página do bfcache, e a mais comum é simples — um header Cache-Control: no-store na resposta HTML. Se o navegador vê isso, ele assume que a página tem dado sensível e nem tenta guardar.

A primeira suspeita (errada): autenticação

Meu blog usa NextAuth, então o primeiro instinto foi óbvio: "deve ser sessão vazando pra dentro da renderização". É um motivo clássico — se uma página lê cookies() ou headers() durante a renderização no servidor, o Next.js automaticamente marca ela como dinâmica e manda Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate. Faz sentido: você não quer que o CDN sirva a mesma página cacheada pra dois usuários com sessões diferentes.

Só que, ao vasculhar o código atrás de cookies(), headers() ou getServerSession() dentro de páginas, achei zero ocorrências. Nenhuma delas lê sessão no servidor — o estado de login inteiro é resolvido no cliente, via useSession(). A teoria não batia.

Comparando headers na unha

Em vez de continuar chutando, fui direto no curl:

# Uma rota que passa pelo nosso middleware
curl -sD - https://prdev.com.br/pt-BR/about -o /dev/null | grep -i cache-control
# Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate

# Uma rota que NÃO passa (excluída no matcher do middleware)
curl -sD - https://prdev.com.br/robots.txt -o /dev/null | grep -i cache-control
# Cache-Control: s-maxage=31536000

A página /about não tem nenhum código relacionado a autenticação. E mesmo assim, vinha com no-store — exatamente igual à home, ao blog, à página de post. A única rota que vinha cacheável de verdade era a que nem passava pelo middleware. Isso apontava pra um único suspeito: o middleware em si.

A virada: eu estava certo pela razão errada

Antes de sair mexendo, fui checar a documentação oficial do Next.js pra confirmar. E foi aí que a investigação ficou interessante — porque meu teste tinha uma falha de metodologia que eu não tinha percebido: comparei /robots.txt (que não passa pelo middleware e também não está sob a rota dinâmica [lang]) contra páginas que passam pelo middleware e estão sob [lang]. Troquei duas variáveis ao mesmo tempo.

A documentação do Next.js diz, sem rodeios:

"Dynamically rendered pages set a Cache-Control header of private, no-cache, no-store, max-age=0, must-revalidate to prevent user-specific data from being cached."

E aí percebi: meu projeto não tem generateStaticParams em lugar nenhum para o segmento [lang]. Isso significa que toda página sob ele — a home, o blog, cada post — já é renderizada dinamicamente por definição, e o Next já aplicaria esse header de qualquer jeito, com ou sem middleware.

O middleware não era a causa raiz. Mas ele acabou sendo, por coincidência, o lugar certo pra corrigir — porque é a única camada que roda antes da resposta final ser montada, e responses de middleware conseguem sobrescrever esse header quando você define explicitamente.

A correção

O detalhe que quase escapou: meu middleware também protege rotas privadas (/blog/exclusive/*). Se eu simplesmente forçasse Cache-Control: public pra tudo, um usuário que desloga e aperta "voltar" poderia ver, por uma fração de segundo, uma versão do bfcache renderizada com a sessão ainda ativa. Então a correção precisava diferenciar os dois casos:

// Rota protegida: nunca sobrescrever. Perder o bfcache aqui é o preço
// certo pra não arriscar reexibir conteúdo autenticado depois do logout.
if (isProtectedPath(pathname)) {
  const token = await getToken({ req, secret });
  if (!token) return NextResponse.redirect(loginUrl);
  return NextResponse.next();
}

// Rota pública: nada aqui depende de sessão, então é seguro
// sobrescrever o no-store automático do Next e liberar o bfcache.
const res = NextResponse.next();
res.headers.set("Cache-Control", "public, max-age=0, must-revalidate");
return res;

Validando de verdade

Rebuild, deploy, Lighthouse de novo:

  • bf-cache sumiu da lista de falhas nas páginas públicas.
  • Testei a rota protegida sem sessão — continua redirecionando pro login normalmente.
  • Conferi o header via curl de novo: public, max-age=0, must-revalidate nas páginas públicas, comportamento antigo intacto nas protegidas.

A lição

Duas, na verdade:

  1. Um teste que "confirma" sua hipótese pode estar confirmando a coisa errada. Eu tinha uma correlação real (middleware ⇒ no-store) mas a causalidade estava incompleta. Só fui pegar isso porque parei pra ler a documentação em vez de só aplicar o fix e seguir em frente.
  2. "Corrigir onde dá" nem sempre é "corrigir a causa" — e tudo bem, desde que você saiba a diferença. O middleware nunca foi o problema, mas era o único lugar com acesso à resposta final antes dela sair. Entender isso é o que evitou eu aplicar a correção errada no lugar errado (por exemplo, tentando mexer em generateStaticParams só pra resolver cache, o que teria sido bem mais invasivo pro que eu realmente precisava).