Cache HTTP na prática: Cache-Control, ETag e revalidação
Como o cache HTTP realmente funciona: frescor, revalidação, ETag e as diretivas de Cache-Control que decidem o que é reusado sem baixar de novo.
Neste artigo
O cache é provavelmente a otimização de maior impacto que existe na web, e também uma das mais mal compreendidas. Quando bem configurado, ele faz uma página que já foi visitada aparecer instantaneamente, sem tocar na rede, porque o navegador reutiliza o que já tem guardado. Quando mal configurado, produz dois problemas opostos e igualmente frustrantes: ou o usuário vê conteúdo velho porque o cache não foi invalidado, ou o servidor recebe requisições demais porque nada é reaproveitado. A diferença entre esses cenários está inteiramente em alguns cabeçalhos HTTP que o servidor envia junto com cada resposta.
O grande equívoco é tratar cache como algo binário: ou o navegador guarda, ou não guarda. A realidade é mais rica e mais útil. O cache HTTP trabalha com dois conceitos distintos que operam em conjunto: o frescor, que determina por quanto tempo uma resposta pode ser usada sem sequer perguntar ao servidor, e a revalidação, que determina como confirmar de forma barata se uma resposta guardada ainda vale quando o frescor expira. Entender que são duas fases diferentes é a chave para configurar cache que é ao mesmo tempo agressivo e correto.
Neste artigo vamos destrinchar essa mecânica. Veremos como o Cache-Control define as regras, como o ETag e o Last-Modified permitem revalidar sem baixar tudo de novo, e como combinar essas peças para servir recursos de forma rápida sem nunca entregar conteúdo desatualizado. É uma otimização que rende muito e custa pouco, desde que você entenda o mecanismo.
Frescor: usar sem perguntar#
A primeira fase do cache é o frescor. Quando o servidor envia uma resposta, ele pode declarar por quanto tempo aquela resposta é considerada fresca. Durante esse período, o navegador usa a cópia guardada diretamente, sem fazer nenhuma requisição — nem sequer para perguntar se algo mudou. Do ponto de vista do usuário, o recurso aparece instantaneamente, porque a rede não é tocada.
O controle disso está no cabeçalho Cache-Control, mais especificamente na diretiva max-age, que informa em segundos por quanto tempo a resposta é fresca. Um valor alto significa que o navegador reaproveitará a cópia por muito tempo sem incomodar o servidor. Um valor de zero significa que a resposta expira imediatamente e sempre precisará ser revalidada antes do uso. Esse número é a alavanca principal de todo o comportamento de cache, e escolhê-lo bem depende de quanto o recurso muda.
O dilema do frescor: agressivo mas correto#
Aqui surge a tensão fundamental. Um max-age alto é ótimo para performance, mas perigoso para conteúdo que muda, porque durante todo o período de frescor o navegador servirá a versão antiga sem checar. Por outro lado, um max-age baixo mantém o conteúdo atualizado, mas desperdiça a maior vantagem do cache, que é evitar a rede. A saída elegante para esse dilema é separar os tipos de recurso. Arquivos cujo conteúdo nunca muda — porque o nome inclui uma impressão digital do conteúdo, mudando o nome sempre que o conteúdo muda — podem receber frescor longuíssimo com segurança, já que uma versão nova terá outro nome. Já o documento HTML que referencia esses arquivos recebe frescor curto ou revalidação obrigatória, garantindo que o usuário sempre pegue as referências mais recentes.
Revalidação: confirmar barato#
Quando o frescor expira, a resposta guardada não é jogada fora imediatamente. Em vez disso, entra a segunda fase: a revalidação. Em vez de baixar o recurso inteiro de novo, o navegador faz uma requisição condicional, perguntando ao servidor se a versão que ele tem ainda é válida. Se for, o servidor responde com um status especial e sem corpo, e o navegador reutiliza a cópia que já tinha. Só se algo realmente mudou é que o servidor devolve o recurso completo.
Essa é uma otimização poderosa porque a requisição de revalidação é minúscula e, quando a resposta é "nada mudou", nenhum byte de conteúdo trafega. O usuário paga apenas o custo de uma ida e volta pequena à rede, em vez do download completo. Para recursos grandes que mudam raramente, isso representa uma economia enorme.
ETag: a impressão digital do conteúdo#
O mecanismo mais preciso de revalidação usa o cabeçalho ETag. Quando o servidor envia um recurso, ele inclui um ETag, que é uma espécie de impressão digital daquela versão específica — tipicamente derivada do conteúdo, de modo que qualquer mudança gera um ETag diferente. O navegador guarda esse valor junto com a resposta.
Na hora de revalidar, o navegador reenvia esse valor no cabeçalho If-None-Match, essencialmente dizendo "eu tenho a versão com esta impressão digital; ela ainda é a atual?". O servidor compara com a versão atual: se baterem, responde 304 Not Modified sem corpo, e o navegador reaproveita sua cópia; se não baterem, responde 200 com o conteúdo novo e um novo ETag. Como o ETag é derivado do conteúdo, ele detecta mudanças de forma exata, sem depender de datas.
Last-Modified: revalidação por data#
Existe um mecanismo alternativo, mais antigo, baseado em tempo. O servidor envia o cabeçalho Last-Modified com a data da última alteração do recurso. Na revalidação, o navegador reenvia essa data no cabeçalho If-Modified-Since, perguntando se houve mudança depois daquele momento. O servidor responde 304 se nada mudou desde então, ou o conteúdo completo se mudou. É menos preciso que o ETag, porque depende da resolução da data e pode falhar em casos de mudanças muito rápidas ou de conteúdo que volta a um estado anterior, mas cumpre o papel para muitos casos. Quando ambos estão presentes, o ETag costuma ter precedência por ser mais confiável.
As diretivas de Cache-Control que importam#
O Cache-Control carrega várias diretivas além do max-age, e conhecê-las é o que permite configurar o comportamento com precisão.
no-storeproíbe qualquer armazenamento: o recurso nunca é guardado, e toda requisição vai ao servidor buscar de novo. É o adequado para dados sensíveis que não devem sobrar em cache algum.no-cacheé enganoso: não significa "não guarde", e sim "guarde, mas sempre revalide antes de usar". A cópia é mantida, mas o navegador confirma com o servidor a cada uso. Combinado comETag, dá conteúdo sempre atualizado com o custo mínimo de uma revalidação.privateindica que a resposta é específica de um usuário e só pode ser guardada pelo navegador dele, nunca por caches compartilhados no caminho, como os de uma rede de distribuição.publicautoriza explicitamente que caches compartilhados guardem a resposta, útil para conteúdo igual para todos.must-revalidatereforça que, uma vez expirado o frescor, a cópia velha não pode ser servida sob nenhuma circunstância sem antes revalidar.immutablepromete que o recurso nunca vai mudar durante seu período de frescor, permitindo ao navegador nem tentar revalidar em recarregamentos.
Caches privados e compartilhados#
Vale distinguir onde o cache vive. O cache do navegador é privado, pertence a um usuário e pode guardar conteúdo personalizado. Já os caches compartilhados — proxies, redes de distribuição de conteúdo — ficam no caminho entre muitos usuários e o servidor, e servem a mesma cópia para todo mundo. Essa distinção é a razão de existirem as diretivas private e public: marcar como private algo destinado só àquele usuário evita o desastre de um cache compartilhado servir os dados de uma pessoa para outra. Confundir isso é uma falha de segurança, não só de performance.
O cabeçalho Vary e as variações da mesma URL#
Há uma sutileza que causa bugs difíceis de diagnosticar: uma mesma URL pode ter respostas diferentes dependendo de certos cabeçalhos da requisição. O mesmo endereço pode devolver conteúdo comprimido ou não, conforme o que o cliente aceita; pode devolver português ou inglês, conforme o idioma pedido; pode devolver a versão para um formato ou outro. Se o cache guardasse uma única resposta por URL, ele acabaria servindo a versão errada — entregando o conteúdo comprimido a um cliente que não sabe descomprimir, ou o idioma errado para outro usuário.
O cabeçalho Vary resolve isso dizendo ao cache quais cabeçalhos da requisição fazem a resposta variar. Quando o servidor declara que a resposta varia conforme a codificação aceita, por exemplo, o cache passa a guardar versões separadas para cada valor daquele cabeçalho, e só serve a cópia guardada para requisições que casam no mesmo valor. Esquecer de declarar corretamente essa variação é uma fonte clássica de bugs em que um usuário recebe a resposta destinada a outro contexto. É um cabeçalho discreto, mas fundamental para que o cache não misture respostas que só parecem iguais por virem do mesmo endereço.
Invalidação: o problema mais difícil#
Existe uma frase famosa que diz que as duas coisas mais difíceis em computação são nomear coisas e invalidar cache, e a segunda metade dessa piada é séria. Depois que uma resposta foi guardada com frescor longo, e o conteúdo por trás dela muda, como fazer o cache soltar a versão velha antes do prazo? A resposta honesta é que, para um cache que já entregou uma resposta fresca ao navegador do usuário, você não tem controle direto: aquela cópia será usada até o frescor expirar, e não há como alcançá-la remotamente.
É por isso que a estratégia de nomear arquivos com uma impressão digital do conteúdo é tão poderosa — ela transforma a invalidação em um não problema. Como cada versão do conteúdo tem um nome diferente, nunca é preciso invalidar nada: a versão nova simplesmente é um recurso novo, com outro nome, que o navegador ainda não tem e vai buscar. A versão velha continua no cache, inofensiva, porque ninguém mais aponta para ela. Em vez de tentar apagar o que está guardado, você garante que o que está guardado nunca precise ser apagado. Essa inversão — projetar para não precisar invalidar, em vez de invalidar — é uma das lições mais valiosas de todo o assunto de cache.
Montando uma estratégia coerente#
Com as peças na mesa, uma estratégia sólida emerge naturalmente da separação entre o que muda e o que não muda. Recursos versionados pelo nome — aqueles cujo nome de arquivo carrega uma impressão digital do conteúdo — recebem frescor longuíssimo e podem ser marcados como imutáveis, porque uma versão nova simplesmente terá outro nome e será baixada como um recurso diferente. Assim o navegador reaproveita a versão antiga para sempre, sem risco, e nunca precisa revalidar.
O documento de entrada, o HTML que aponta para todos esses recursos, segue a lógica oposta. Ele precisa estar sempre atualizado, porque é ele que decide quais versões dos outros recursos serão carregadas. Então recebe frescor curto ou revalidação obrigatória, tipicamente com no-cache mais ETag: o navegador guarda, mas confirma a cada uso, pagando só o custo de uma revalidação barata e recebendo 304 quando nada mudou. Dados de API ficam entre os dois extremos, com frescor ajustado à velocidade com que mudam e revalidação por ETag para o resto.
Fechando#
O cache HTTP é uma daquelas ferramentas em que um pouco de entendimento rende muito resultado. A distinção central — frescor para usar sem perguntar, revalidação para confirmar barato — organiza todo o resto. O Cache-Control define as regras de frescor e de comportamento; o ETag e o Last-Modified habilitam a revalidação condicional que devolve 304 em vez do conteúdo inteiro; e as diretivas finas ajustam onde e como cada resposta pode ser guardada.
O que torna esse conhecimento valioso na prática é que ele resolve os dois problemas opostos de uma vez. Configurando frescor longo para o que não muda e revalidação para o que muda, você consegue cache agressivo sem entregar conteúdo velho. O usuário ganha páginas que aparecem na hora, o servidor ganha alívio de tráfego, e você ganha a tranquilidade de saber que uma atualização vai chegar quando precisar. Não é preciso escolher entre rápido e correto: a mecânica do cache HTTP foi feita justamente para entregar os dois ao mesmo tempo, desde que você entenda em qual das duas fases cada recurso deve viver.