in ASP.NET

Cache + .NET: Cache HTTP (aumente a performance de seu site)

O Cache HTTP é um recurso extremamente importante e disponível em qualquer plataforma de desenvolvimento web, neste quarto post da série de Cache + .NET vou apresentar o funcionamento do Cache HTTP e as principais maneiras de implementa-lo utilizando .NET.

Front End (80-20)

Para falar sobre Cache HTTP, vou começar com uma frase sobre performance de front end, de um dos maiores especialitas em web performance.image

“…somente 10-20% do tempo total do carregamento de uma página é gasto para receber o HTML do servidor para o navegador. Você precisa focar nos outros 80-90% se você quiser tornar suas páginas visivelmente mais rápidas…”

(Steve SoudersHigh Performance Web Sites)

Neste livro Steve Souders explica 14 regras, que são boas práticas para aumentarmos a performance de nossos sites, configurar e utilizar o Cache HTTP nos ajuda a evitar downloads de recursos e conexões desnecessários com o servidor.

Cache HTTP

O Cache HTTP é uma das principais maneiras de aumentar a performance de nossas aplicações web, ele funciona como um mecanismo para avisar o navegador que ele pode armazenar uma cópia local daquele recurso (com um mecanismo de expiração), o que evita que o download, e, ou a requisição seja feita novamente para aquele recurso dentro de um intervalo de tempo (configurável).

Para isso, o protocolo HTTP 1.1 nos permite utilizar duas maneiras de definir por quanto tempo um recurso será valido: o header Expires e o header Cache-Control: max-age.

O header Expires serve para definirmos a data em que aquele recurso irá expirar (ex: Expires: Thu, 21 Dec 2013 16:00:00 GMT).

Já o Cache-Control serve para definirmos por quanto tempo (em segundos) aquele recurso irá permanecer válido no navegador do cliente, (ex: Cache-Control:max-age=2592000) => 30dias. No Cache-Control existem algumas configurações que podem ser utilizadas para definir em quais os níveis o Cache pode ser armazenado:

Private: aquele recurso pode ser armazenado em um mecanismo de cache privado (no caso no navegador do cliente). Na maioria dos servidores de proxy essa informação não será armazenada.

Public: o recurso pode ser cacheado em mecanismos de cache compartilhados (servidores proxy) e no navegador do cliente.

No-cache: o recurso não será armazenado no cache, nem mesmo se for solicitado pelo mesmo cliente.

No-store: a função do No-Store é prevenir a retenção de informações sensíveis, ele pode ser enviando tanto na requisição quanto na resposta, caso ele seja enviado em um requisição, os mecanismos de cache não podem armazenar nenhuma parte da requisição, o mesmo acontece se ele for enviado na resposta.

( * )Se você utilizar o Cache-Control: max-age, ele sobrescreverá a configuração do Expires.

 

 

GET Sem Cache

Abaixo segue o exemplo de uma requisição sem nenhuma configuração de cache.

image

Notem que na próxima requisição para o mesmo recurso, ele será baixado novamente:

image

 

GET Condicional ( * )

Existe dois headers que o servidor web pode utilizar para entender que aquele recurso não mudou. O Last-Modified (data de última modificação do arquivo) e a Etag (espécie de hash gerado pelo servidor com base no arquivo).

Na requisição, esses headers são passados do servidor para o navegador que os armazena junto do recurso.

image

Na próxima requisição, esses headers são retornados para o servidor, que analisa eles e, caso os conteúdos não tenham sido modificados, o servidor responde aquela requisição com o status 304 Not Modified, com isso economizamos um download de 200K do arquivo.image

Esse método economiza o download, porém o navegador ainda realiza uma requisição para validar de aquele recurso foi modificado, o que causa uma perda de performance.

GET Cache Configurado

No exemplo abaixo, na resposta de um arquivo js, é adicionado o Cache-control, com o valor do max-age, o que garante que na próxima requisição para aquele recurso, o navegador não execute a requisição e utilize o recurso do cache.

image

Com o Cache-control configurado, dentro do período, o navegador não executará a requisição.

image

GET no-cache

Como eu disse anteriormente, é importante conhecer a configuração no-cache, que é utilizada na resposta do exemplo abaixo.

image

Utilizar o no-cache garante que aquele recurso não seja cacheado no navegador, isso é útil em cenários de arquivos dinâmicos, e que precisamos ter sempre o recurso renovado.

image

 

Cache HTTP + .NET

No ASP.NET um handler (.ASHX) possui diversas funções, com ele é possível tratar as requisições e as respostas no mais baixo nível, com isso podemos utilizar handlers para retornar imagens, arquivos físicos, css, js e muitos outros conteúdos.

No exemplo abaixo eu criei um .ashx que lê o binário de uma imagem(simulando um acesso ao banco) e retorna ela para o usuário, eu poderia ter utilizado uma Action, ou uma Página (.aspx).

public class Imagem : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        //acesso ao banco
        var binarioDaImagem =  ReadFile(context.Server.MapPath("~/Content/teste.jpg"));

        context.Response.ContentType = "image/jpg";
        context.Response.BinaryWrite(binarioDaImagem);
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
    protected byte[] ReadFile(string sPath)
    {
        byte[] data = null;
        FileInfo fInfo = new FileInfo(sPath);
        long numBytes = fInfo.Length;
        FileStream fStream = new FileStream(sPath, FileMode.Open, FileAccess.Read);
        BinaryReader br = new BinaryReader(fStream);
        data = br.ReadBytes((int)numBytes);
        br.Close();
        fStream.Dispose();
        return data;
    }
}

Se fizermos uma requisição para este handler, podemos ver que o recurso será baixado a cada requisição que o usuário fizer:

Request 1

image

Notem que o handler está respondendo o Header da seguinte maneira: “Cache-Control: private” o que indica que aquele conteúdo da requisição não deve ser armazenado em um servidor proxy, e pode ser cacheado pelo navegador, desde que seja utilizado em conjunto com o max-age.

Porém na resposta ele não informa nenhum parâmetro de quanto tempo o conteúdo deve ser cacheado, com isso, na próxima requisição, o conteúdo é baixado novamente.

Request 2

image

 

O ASP.NET fornece uma serie de configurações para adicionarmos os headers de expiração e configurarmos o Cache HTTP, podemos fazer isso adicionando o header manualmente:

//Adicionando o cache manualmente via Headers
var dataExpiracao = DateTime.Now.AddDays(30);
context.Response.Headers.Add("Expires", dataExpiracao.ToString("R")); //Thu, 21 Dec 2013 16:00:00 GMT
context.Response.Headers.Add("Cache-Control", "max-age=2592000");

Ou podemos utilizar a maneira recomendada, que é utilizando uma propriedade do HttpResponse, o HttpResponse.Cache, que nos oferece uma maneira abstraída de utilizar e configurar o Cache HTTP:

//Utilizando a propriedade Cache do Response
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetExpires(DateTime.Now.AddDays(30));
context.Response.Cache.SetMaxAge(new TimeSpan(30, 0, 0, 0));
context.Response.Cache.SetSlidingExpiration(true);

Ao utilizarmos as configurações a cima, teremos o handler da seguinte maneira:

public class Imagem : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        //acesso ao banco
        var binarioDaImagem = ReadFile(context.Server.MapPath("~/Content/teste.jpg"));

        context.Response.ContentType = "image/jpg";

        //Utilizando a propriedade Cache do Response
        context.Response.Cache.SetCacheability(HttpCacheability.Public);
        context.Response.Cache.SetExpires(DateTime.Now.AddDays(30));
        context.Response.Cache.SetMaxAge(new TimeSpan(30, 0, 0, 0));
        context.Response.Cache.SetSlidingExpiration(true);

        context.Response.BinaryWrite(binarioDaImagem);
    }

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }
    protected byte[] ReadFile(string sPath)
    {
        byte[] data = null;
        FileInfo fInfo = new FileInfo(sPath);
        long numBytes = fInfo.Length;
        FileStream fStream = new FileStream(sPath, FileMode.Open, FileAccess.Read);
        BinaryReader br = new BinaryReader(fStream);
        data = br.ReadBytes((int)numBytes);
        br.Close();
        fStream.Dispose();
        return data;
    }
}

Agora, quando um usuário faz a primeira requisição, os Headers que configuramos avisam para o navegador que aquele recurso pode ser mantido por 30 dias no Cache.

Request 1

image

 

Request 2

Na segunda requisição, o servidor nem chega a ser solicitado, e o conteúdo é retornado do cache do navegador.

image

IIS

Após ver como adicionar esses headers com C#, também é possível configurar esses header em diretórios específicos no IIS, para isso, podemos fazer esta configuração via interface.

Para isso precisaremos ir no IIS, abrir o website e selecionar o diretório no qual queremos configurar os cabeçalhos, e utilizar a opção Cabeçalhos de Resposta HTTP:

image

E nele utilizar a opção Definir Cabeçalhos Comuns, na janela de configuração marcar a opção Expirar conteúdo da Web, e no meu caso, configurar que ele expire em 30 dias:

image

 

Ou podemos inserir um web.config no diretório, com as configurações de Cache que devem ser adicionadas nos arquivos daquele diretório:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <staticContent>
            <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="30.00:00:00" />
        </staticContent>
    </system.webServer>
</configuration>

 

Dicas

  1. Utilize Cache HTTP em CSS, Imagens, JS e onde puder (com tempo de 1 mês a 1 ano)
  2. Tenha uma estratégia de renovação e versionamento do cache, como por exemplo troca do nome do recurso, e ou parâmetros na url. Não adianta pedir para seu cliente apertar um Ctrl + F5
  3. Utilize ferramentas como Page Speed e YSlow para validar e testar suas aplicações
  4. Utilizar o Cache faz com que as requisições deixem de ser feitas para seu servidor, não faz sentido seu usuário fazer download do logo do site ou do css principal a cada pagina navegada.

Bom espero que este post seja útil, estou a disposição para duvidas, criticas e sugestões.

abs

Rodolfo

  • Anonymous

    quando eu seto no webconfig clientCache automaticamente o css, imagens e js ja ficarao em cache?

  • http://www.rodolfofadino.com.br/ Rodolfo Fadino

    Elton, quando você configura o clientCache, o IIS adicionará os headers de expiração nos conteúdos estáticos (css, js, img, etc) do diretório que tiver o web.config.

  • Rômulo Innocêncio

    Gostei, mas eu entendo pouco…
    Estou com um problema parecido também, vejam se podem ajudar
    http://stackoverflow.com/questions/30466438/gzip-iis-7-5-locaweb

  • Tunico Paschoalão

    Rodolfo, parabéns pela iniciativa.
    Fiquei como uma dúvida, minhas imagens, arquivos css e js estão em sub pastas, como eu faço para configurar também para esses casos e não somente onde está o web.config?

  • http://www.rodolfofadino.com.br/ Rodolfo Fadino

    Tunico, você pode configurar em qualquer diretório, colocar um web.config dentro da pasta /css por exemplo

  • Daniela Moreira

    Existe outra maneira de renovar o cache sem trocar o nome do arquivo em asp.net mvc 4?