Etag - Cache de arquivos no navegador

Artigo que apresenta a diretiva HTTP ETag e seu funcionamento para guardar cache de arquivos no navegador.

performance
Introdução

No primeiro artigo "Expires no Apache - Cache de arquivos no navegador", vimos como configurar o apache para utilizar o mod_expires para sugerir o cache de arquivos estáticos no navegador, com base na data de modificação. No segundo artigo "Expires no PHP - Cache de arquivos no navegador", vimos algo similar, mas aplicado a arquivos gerados dinamicamente.

Neste artigo, veremos um outro mecanismo usado para sugerir o armazenamento de um documento no cache do navegador. Este mecanismo é chamado ETag (Entity Tag).

Funcionamento do ETag

O funcionamento básico do cacheamento usando ETag é o seguinte:

  • O navegador solicita um documento no servidor pela primeira vez.
  • O servidor devolve o documento e, junto com ele, informa um Hash indicando a "versão" do documento.
  • O navegador recebe o documento e o guarda em cache, juntamente com o Hash recebido.
  • O navegador solicita o mesmo documento no servidor, informando o Hash que ele possui.
  • O servidor recebe o Hash e verifica se o documento continua com o mesmo Hash. Se o Hash continua idêntico, o servidor informa ao navegador que ele pode usar o documento que ele tem em cache. Se o Hash mudou, o servidor devolve o documento inteiro, juntamente com seu novo Hash.

Note que o funcionamento é similar. A diferença é que se usa o Hash para determinar se um documento continua válido ou não.

O benefício do uso do ETag é uma redução significativa no tráfego de dados entre cliente e servidor.

Assim como a utilização do Expires, o ETag pode ser usado tanto para documentos estáticos quanto para documentos gerados dinamicamente. Nas próximas seções veremos como configurar o Apache para utilizar ETag no conteúdo estático e como implementar ETag nos scripts PHP que geram conteúdo dinâmico.

ETag para documentos estáticos

Para documentos estáticos, o ETag pode ser configurado no servidor HTTP. No Apache, isso é feito através da diretiva de configuração FileETag. A diretiva deve representar quais elementos serão usados para montar o ETag de cada arquivo, que podem ser:

  • Inode: leva em conta o Inode do arquivo no sistema de arquivos do servidor.
  • MTime: leva em conta a data de modificação do arquivo no servidor.
  • Size: leva em conta o tamanho do arquivo.
  • All: leva em conta todos os elementos possíveis.
  • None: não inclui o ETag no arquivo.

A diretiva é configurada com a lista destes elementos separados por espaço e, opcionalmente, prefixados por "+" ou "-" indicando "levar em conta" e "não levar em conta", respectivamente.

A configuração deve ser aplicada a um grupo de arquivos específico, portanto, você deve especificar quais arquivos receberão esta configuração através de uma seção <File> ou <FileMatch>, conforme o exemplo, que aplica o ETag nos arquivos com extensão .gif, .jpg, .png, .css, .js, .pdf e .txt:

<FilesMatch "\.(gif|jpg|png|css|js|pdf|txt)">
    FileETag All
</FilesMatch>

Um problema com o ETag é que alguns servidores colocam os documentos estáticos distribuídos em vários servidores, para atender à alta demanda com boa performance. Só que em cada servidor, uma determinada cópia do arquivo provavelmente terá Inode diferente e data de última modificação diferente, apenas o tamanho igual. Porém, levar em conta apenas o tamanho do arquivo é perigoso já que um arquivo com uma letra errada pode ser corrigido e manter o mesmo tamanho. Uma possível solução é usar apenas o Expires, ou então usar MTime e Size, mas garantir que o MTime de todos servidores esteja igual (isso pode ser feito com o comando touch no Linux, que modifica a data de última modificação para uma data específica). Por exemplo:

$ touch -d "2013-03-06 20:46:00" arquivo.php

ETag para documentos dinâmicos

Para usufruir do recurso ETag em documentos dinâmicos, precisamos implementar a camada de negociação do servidor com o navegador.

Primeiramente, precisamos definir um mecanismo para geração do hash. Felizmente, com programação, podemos usar qualquer elemento para isso. Um bom exemplo é usar o algoritmo MD5 do conteúdo gerado. Você pode preferir concatenar com o tamanho do conteúdo gerado, para dificultar ainda mais as chances de hashes idênticos para o mesmo conteúdo. A restrição é que este hash precisa ser delimitado por aspas duplas. Então, antes de enviar o documento para o navegador, informamos pelo cabeçalho HTTP o Hash gerado, conforme o exemplo:

// Armazenamos todo conteudo do documento numa variavel
$documento = ...;

// Gerando o hash do documento
$hash = '"' . md5($documento) . '"';

// Enviando o cabecalho HTTP
header('Content-Type: text/html; charset=UTF-8');
header('ETag: ' . $hash);

// Enviando o documento
echo $documento;
exit(0);

Se você já tem uma aplicação que realiza vários "echo", pode capturar todo conteúdo do documento através da utilização de Buffer do Output.

Bom, ao receber esta ETag, o navegador vai enviá-la para o script quando for acessá-lo novamente. Você pode capturar este valor pela variável superglobal $_SERVER['HTTP_IF_NONE_MATCH']. Então, o que temos que fazer é checar se recebemos este valor e, se recebemos, verificar se ele é válido (se é igual ao hash do documento que acabamos de gerar). Se o hash for válido, devolvemos o cabeçalho HTTP 304, indicando que o documento não foi modificado (da mesma forma que fizemos com o Expires):

// Armazenamos todo conteudo do documento numa variavel
$documento = ...;

// Gerando o hash do documento
$hash = '"' . md5($documento) . '"';

// Se o navegador possui um hash valido: informar que o documento nao mudou
if (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER) && $_SERVER['HTTP_IF_NONE_MATCH'] == $hash) {
    header('HTTP/1.1 304 Not Modified');
    header('Date: ' . gmstrftime('%a, %d %b %Y %T %Z', $_SERVER['REQUEST_TIME']));
    header('Cache-Control: ');
    header('Pragma: ');
    header('Expires: ');
    exit(0);
}

// Enviando o cabecalho HTTP
header('Content-Type: text/html; charset=UTF-8');
header('ETag: ' . $hash);

// Enviando o documento
echo $documento;
exit(0);

Talvez a diretiva "If-None-Match" passada pelo navegador não é colocada em $_SERVER['HTTP_IF_NONE_MATCH'] em algumas versões do PHP ou SAPI usada. Então, você pode precisar percorrer o cabeçalho HTTP enviado pelo navegador através de uma destas funções: getallheaders, apache_request_headers ou http_get_request_headers.

Observe que o processamento para gerar o documento continua existindo. O ganho em performance está em não precisar enviar todo o documento novamente para o navegador, se ele informou que já tem uma versão válida do mesmo.

Observações

O ETag pode ser usado em conjunto com o Expires. A especificação do HTTP diz que o cabeçalho 304 (documento não modificado) deve ser enviado apenas quando satisfazer as duas condições, ou seja, a validade do documento não expirou e o ETag continua o mesmo.

1 comentário