Todos os artigos
152 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Tutoriais

Cache do navegador: Cache-Control e ETag

Como o navegador decide reusar um recurso: Cache-Control, max-age, no-cache vs no-store, ETag e 304. A estratégia de cache que acaba com o bug do antigo.

Cache do navegador: Cache-Control e ETag
COVER · Tutoriais

Você corrige um bug, faz deploy, abre o site e está perfeito. Aí chega o cliente dizendo que ainda vê a versão antiga, com o erro intacto. Você manda ele dar um "Ctrl+Shift+R" e o problema some — mas não dá pra pedir hard refresh pra cada visitante. Esse cenário, o clássico "atualizei e o usuário continua vendo o antigo", quase nunca é um bug de código: é cache mal configurado. O navegador está fazendo exatamente o que você mandou, só que você mandou a coisa errada.

Este post explica como o navegador decide reusar um recurso em vez de baixar de novo, o que cada diretiva de Cache-Control realmente faz, e qual é a estratégia que resolve o problema de invalidação de uma vez.

Como o navegador decide reusar um recurso

Quando o navegador precisa de um arquivo (um JS, uma imagem, o HTML), ele primeiro olha o que tem guardado localmente. A decisão se ramifica em três caminhos:

  1. Cache fresco — a cópia local ainda é considerada válida. O navegador serve direto do disco, zero request vai pra rede. É o caminho mais rápido que existe: nenhuma viagem ao servidor.
  2. Cache obsoleto (stale) — a cópia expirou, mas o navegador tem como perguntar "ainda vale?" antes de baixar tudo de novo. Isso é a revalidação.
  3. Sem cache — não tem cópia, ou a política proíbe reusar. Baixa do zero.

O servidor controla qual caminho será seguido através de um header de resposta: o Cache-Control. É ele que define por quanto tempo a cópia é fresca e o que fazer quando ela deixar de ser.

Cache-Control: o que cada diretiva faz de verdade

O Cache-Control é uma lista de diretivas separadas por vírgula. As que importam no dia a dia:

Cache-Control: public, max-age=31536000, immutable
  • max-age=N — define a janela de frescor em segundos. max-age=31536000 são 365 dias. Durante essa janela, o navegador serve direto, sem tocar no servidor.
  • public vs private — public permite que caches intermediários (CDN, proxy) guardem o recurso. private restringe ao cache do navegador do usuário; use pra conteúdo personalizado, tipo uma página com dados da conta logada.
  • immutable — promete que o conteúdo desse URL nunca vai mudar enquanto for fresco. Isso impede que o navegador revalide mesmo quando o usuário aperta F5. Sem immutable, um reload normal dispara uma revalidação condicional; com ele, o navegador nem pergunta.

no-cache vs no-store — a confusão clássica

Esses dois nomes enganam. no-cache não significa "não faça cache". Significa "pode guardar, mas sempre revalide antes de servir". O navegador mantém a cópia e, a cada uso, pergunta ao servidor se ela ainda vale — economizando o download do corpo quando nada mudou.

no-store é o "de verdade não guarde nada". Nenhuma cópia em lugar nenhum. É pra dados sensíveis (tokens, respostas bancárias), não pra controle de versão de assets. Se você quer que o HTML sempre revalide mas ainda aproveite o cache quando nada mudou, o que você quer é no-cache, não no-store.

Cache fresco vs revalidação

Vale martelar a diferença, porque é onde mora o ganho de performance.

Com cache fresco (dentro do max-age), o navegador não fala com o servidor. Latência zero de rede pra aquele recurso. É o ideal pra qualquer coisa que não muda: bibliotecas, fontes, imagens versionadas.

Com revalidação, a cópia expirou mas o navegador não joga fora. Ele faz um request condicional perguntando "isso mudou?". Se não mudou, o servidor responde sem reenviar o corpo — você paga uma viagem de rede curta em vez do download inteiro. É um meio-termo: mais lento que cache fresco, muito mais rápido que baixar tudo.

A revalidação é o mecanismo certo pro HTML, que muda a cada deploy mas que você não quer rebaixar a cada visita se nada mudou.

ETag e Last-Modified: como o servidor responde 304

A revalidação precisa de uma forma de o servidor dizer "não mudou" sem reenviar o arquivo. São dois mecanismos.

ETag é um identificador opaco do conteúdo — um fingerprint, normalmente um hash do corpo da resposta:

ETag: "abc123"

Na próxima vez que o navegador for revalidar, ele manda esse valor de volta no header If-None-Match:

If-None-Match: "abc123"

O servidor compara o ETag que ele tem agora com o que o navegador mandou. Se forem iguais, responde:

304 Not Modified

Um 304 é uma resposta sem corpo. Os headers vão, o conteúdo não. O navegador entende "use a cópia que você já tem" e o usuário não paga o download. Esse 304 Not Modified é o coração da revalidação — quando estiver depurando comportamento de cache, consultar o significado dos status HTTP ajuda a distinguir um 304 (revalidado, ótimo) de um 200 que voltou a baixar tudo.

Last-Modified é o mecanismo mais antigo, baseado em data. O servidor manda Last-Modified: <data>, o navegador devolve If-Modified-Since: <data>, e se o arquivo não foi modificado desde então vem o mesmo 304. ETag é mais preciso (detecta qualquer mudança de byte, não só timestamp), mas os dois coexistem e o navegador usa o que estiver disponível.

A estratégia vencedora: fingerprint + max-age longo + immutable

Aqui está a configuração que resolve o problema de invalidação de uma vez, e é o que todo bundler moderno (Vite, webpack, esbuild) já faz por padrão.

Para assets (JS, CSS, imagens), gere o arquivo com um hash no nome:

app.abc123.js

O abc123 é um fingerprint do conteúdo. Se uma linha de código muda, o hash muda, e o nome do arquivo muda — vira app.def456.js. Aí você serve esses assets com:

Cache-Control: public, max-age=31536000, immutable

Cache agressivo, um ano, nunca revalida. E está tudo bem justamente porque o nome carrega a versão: o navegador nunca precisa perguntar se app.abc123.js mudou, porque por definição ele nunca muda. Quando o conteúdo muda, é um arquivo diferente, com URL diferente, que o navegador baixa pela primeira vez.

O problema da invalidação some porque você nunca invalida nada — você publica um novo nome. É por isso que o hash vai no nome do arquivo, e não em algum query string mágico.

Para o HTML, a regra se inverte:

Cache-Control: no-cache

O HTML é o ponto de entrada que aponta pros assets versionados. Ele precisa sempre revalidar, porque é ele que diz ao navegador "agora carregue app.def456.js em vez de app.abc123.js". Combinado com ETag, na maioria das visitas o HTML volta como 304 — barato — mas no deploy seguinte ele pega o novo conteúdo na hora, e junto vêm as referências aos novos assets.

Essa dupla — assets imutáveis com hash no nome, HTML sempre revalidado — é também uma das alavancas mais fortes pra performance percebida, tema que detalho em reduzir o tempo de carregamento inicial.

Opinião: cacheie assets agressivamente, nunca o HTML

Minha regra, sem meio-termo: cacheie assets versionados o mais agressivamente possível (max-age de um ano, immutable) e nunca trate o HTML como imutável. Quase todo bug de "atualizei e o usuário continua vendo o antigo" cai numa de duas armadilhas: ou o HTML foi cacheado com max-age longo (então o navegador serve a página velha apontando pros assets velhos), ou os assets não têm hash no nome (então o navegador reusa app.js antigo mesmo depois do deploy).

A solução não é desligar o cache — desligar cache é jogar performance fora pra resolver um problema de configuração. A solução é versionar os assets e revalidar o HTML. Quando isso está certo, você nunca mais pede hard refresh pra ninguém.

Perguntas frequentes

Qual a diferença entre no-cache e no-store?

no-cache permite guardar a cópia, mas obriga a revalidar com o servidor antes de cada uso — quando nada mudou, você ganha um 304 e economiza o download. no-store proíbe guardar qualquer cópia, em qualquer lugar; cada request baixa tudo de novo. Use no-cache pra HTML e conteúdo que muda mas se beneficia de revalidação; reserve no-store pra dados sensíveis que não podem ficar em disco.

Por que o hash vai no nome do arquivo e não numa query string?

Porque o nome do arquivo é a forma mais confiável de identidade de cache em toda a cadeia (navegador, CDN, proxy). Alguns caches intermediários ignoram ou tratam de forma inconsistente recursos que diferem só na query string, então app.js?v=abc123 pode não invalidar de verdade. Mudar o nome do arquivo (app.abc123.js) é inequívoco: é um recurso novo, com URL novo, sem ambiguidade pra ninguém na cadeia.

O immutable funciona em todos os navegadores?

O immutable é uma dica: navegadores que o entendem pulam a revalidação mesmo num reload normal, o que evita uma rodada de requests condicionais quando o usuário aperta F5. Navegadores que não entendem simplesmente ignoram a diretiva e caem no comportamento padrão do max-age — ou seja, não quebra nada. Combinado com hash no nome, o ganho é real e o risco é zero.

Como sei se a revalidação está funcionando?

Abra o painel Network do DevTools e olhe o status e o tamanho de cada recurso. Um recurso fresco aparece como servido do cache (sem ida à rede); um revalidado com sucesso aparece como 304 Not Modified com corpo de tamanho zero; um que voltou a baixar aparece como 200 com o tamanho cheio. Ver muitos 200 onde deveria haver 304 é sinal de ETag ausente ou Cache-Control mal configurado.

Resumo prático

O navegador reusa um recurso quando o Cache-Control diz que ele ainda é fresco, e revalida com ETag/If-None-Match quando expirou, recebendo um 304 Not Modified se nada mudou. A estratégia que elimina os bugs de cache é simples: assets com hash no nome servidos com public, max-age=31536000, immutable, e HTML com no-cache pra sempre revalidar. Cache agressivo onde a versão está no nome, revalidação onde o conteúdo aponta pro resto. Faça isso e o "atualizei e o usuário vê o antigo" deixa de existir.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil