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.
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:
- 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.
- 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.
- 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=31536000são 365 dias. Durante essa janela, o navegador serve direto, sem tocar no servidor.publicvsprivate—publicpermite que caches intermediários (CDN, proxy) guardem o recurso.privaterestringe 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. Semimmutable, 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.
- 01 Nubank Croma: o que vem no plano, quanto custa e pra quem realmente compensa O Nubank lançou o Croma, um plano de média renda entre o cartão gratuito e o Ultravioleta. Veja o que está incluído, o cashback real, os R$ 39 de mensalidade (e como zerar) e faça a conta antes de aderir.
- 02 Nameserver e zona DNS: qual é a diferença Trocar o nameserver não edita a sua zona DNS: muda onde ela vive e deixa os records para trás. Por isso o e-mail quebra. Entenda a diferença de uma vez.