Arquivo

Archive for setembro \12\UTC 2011

Por uma web mais rápida: compressão radical de JavaScript – parte 2

No post anterior, descrevi o cenário em que um código em JS deveria ser entregue com um nível de compressão excepcional, visto que ele seria bastante requisitado. Expliquei um pouco do funcionamento do Packer para JavaScript e dei algumas dicas para otimizar o uso dele de forma que não pese no lado do cliente.

Neste ponto, um código de 36 KB (XRegExp + parte da aplicação) já está razoávelmente comprimido, mas não o suficiente para um ambiente com muitas requests: advertising.

A solução encontrada foi compressão pelo servidor com GZip. Otimização, aliás, recomendada no post do Sérgio Lopes e não só por ele!

É uma solução muito eficaz para entrega de conteúdo estático. Se utilizada com cache, então, fica lindo. Mas essa opção não se adequava ao caso, já que o JS era “eventualmente estático” e qualquer alteração das regras impactava no conteúdo do JS, seja modificando o tamanho para mais ou menos, mas, sempre modificando o arquivo. Daí a necessidade de não cachear esses arquivos e garantir que toda alteração feita pelo usuário da plataforma seria imediatamente refletida no script.

Então foi decidido: compressão máxima pelo servidor onde os arquivos JS (um para cada site, cada cliente com vários sites) seriam hospedados. Packer  + Gzip nível 9 – compressão de 36 para menos de 4 KB. Incrível! (~4 KB * clientes * sites * milhões) já parece ser um número bem melhor!

Nesse ponto, fiquei incomodado – mesmo sem ter feito qualquer benchmark – sobre como o servidor responderia tendo que efetuar tantas compressões on-the-fly, sem caching e acabei chegando a uma solução muito, mas muito simples e eficaz:

Para conteúdos estáticos e muito requisitados, não deixe o servidor comprimir durante a requisição. Entregue comprimido.

Sim, isso! Armazenar o arquivo já comprimido! O backend já era responsável por obter as regras, concatenar a XRegExp, as regras e o código que faz tudo funcionar; agora também é responsável por comprimir o JS com GZip. Gastar algum tempo de processamento comprimindo 36 KB (ou mais) em 4 KB? Nunca na requisição, só no momento em que uma regra for modificada ou em algum cron job na madrugada executado por garantia.

E o servidor?

A não ser que ele seja instruído a não fazer isso, e manter a compressão habilitada para tudo, ele vai comprimir o que já foi comprimido. Então, aqui está o maior truque disso tudo:  configurar o servidor para caso uma request peça por um arquivo armazenado em determinado diretório, deixe a compressão e envie uma header Content-Encoding: gzip.

E aquele browser que não tem suporte a gzip?

Não pode entregar como gzip. Excepcionalmente, o backend também trata de gerar os scripts somente com a compressão do Packer. Nesse caso, usar cache é importante, porque uma parcela de usuários receberá um arquivo um pouco maior que os outros e é importante não deixar a performance do servidor ser impactada por isso.

Usamos  o Lighttpd e foi muito fácil configurá-lo para agir assim. Para caching, o escolhido foi o Varnish, mas como não fui responsável pela configuração, não posso dar detalhes.

O conceito mais importante dessa parte do post não se aplica ao JS ou ao comportamento da aplicação, mas do servidor. A idéia de entregar conteúdo já comprimido ao invés de utizar compressão on-the-fly se aplica a qualquer tipo de conteúdo que não seja interessante, do ponto de vista da performance, de ser entregue através de métodos específicos de caching. E a conclusão é de que isso não é mais do que um método alternativo de caching.

Considerei a abordagem de usar caching mais forte, separando a XRegExp, com um tempo de cache maior, enquanto as regras e o código ficariam com um tempo menor de cache. Mas aí não seria tão “emocionante” e se por algum motivo a XRegExp não fosse carregada, a aplicação não funcionaria.

Concluído o projeto, me desliguei da empresa com uma proposta mais interessante, deixando uma documentação bastante forte sobre essas idéias. Preciso dar uma olhada nos sites dos clientes para ver se eles já estão utilizando tal aplicação!

Por uma web mais rápida: compressão radical de JavaScript – parte 1

Post inspirado pela palestra do Sérgio Lopes sobre otimização de sites, que foi apresentada na #QConSP. Gostaria de ter demonstrado esse caso no momento, como sugestão para todos da área, mas eu sou tímido. 😛

Em uma empresa onde trabalhei no ano passado, tive que desenvolver um código JS que usa uma biblioteca adicional, XRegExp, para named capture em expressões regulares. Juntando essa biblioteca e meu código, o tamanho total ficava em cerca de 36 KB – no mínimo, já que o código era modificado por variáveis que o cliente da plataforma poderia configurar. A projeção de hits no servidor solicitando um JS era da ordem de milhões por mês, dado que é uma plataforma de advertising. Considerando que cada site deve incluir esse JS e um cliente tem vários sites: (+36 KB * clientes * sites * milhões): não resulta em um número muito bom.

* Detalhe importante: o JS não é totalmente estático. Caso o cliente da plataforma modificasse uma regra, ela teria que ser imediatamente aplicada ao JS, logo, não poderia ficar muito tempo em cache do usuário. O jeito encontrado foi servir sem cache e com o máximo possível de compressão.

Há 3 anos eu conheci o Packer, um compactador de JavaScript que é bastante utilizado por frameworks JS e módulos de pipelining em frameworks web – detalhe que na época eu o conheci através de um código malicioso e como eu queria saber o que o código estava fazendo, então tive que estudar a forma como ele era compactado e o que significava aquela function(p,a,c,k,e,r) no começo do arquivo. Ele tem muito valor e utilidade em ambientes onde você precisa fazer entrega de conteúdo com eficiência.

Ele não é tão simples como outros códigos que eliminam comentários, espaços, tabs, pontos-e-vírgulas duplicados, quebras de linha, deixando a estrutura básica da linguagem para que o código seja funcional. Ele faz bem mais que isso. Explico agora:

– O Packer parseia seu código JS, fazendo tudo o que eu descrevi ali em cima e também separa cada palavra-chave ou valor, montando um array* de strings.
– Cada palavra armazenada no array possui um índice numérico e esse índice é usado como referência no código que o Packer gera.
– O Packer cria um wrapper, que é a função anônima (p,a,c,k,e,r), responsável por descompactar o código gerado, buscar as referências no array e remontar o código, substituindo as referências pelos valores respectivos, terminando com um eval() para interpretar o código remontado.
– Como as referências são únicas e as strings usadas para descrevê-las são pequenas, muitas vezes usando números de base 62 – dependendo da configuração, o código gerado é bastante econômico e sucinto.

* Este array de strings é na verdade uma string só, com seus valores delimitados por pipes

Vou dar um exemplo bem simples de como o packer vai montar um o código compactado:

function stack_messages(strings) {
var i = 0;
for (i = 0; i < strings.length(); i++) {
var message = “Annoying message #” + i + “: ” + strings[i]”;
alert(message);
}
}

Esse é o “array” de palavras úteis que o Packer gerou após fazer o parsing:

‘|i|var|function|stack_messages|for|length|Annoying|message|alert||’

E gerar o seguinte código:

‘3 4(a){2 1=0;5(1=0;1<a.6();1++){2 b=”7 8 #”+1+”: “+a[1]”;9(b)}}’

Não parece, mas esta é a função stack_messages. Ignorando chaves, pontos-e-vírgulas, parênteses e outros símbolos que obviamente não podem ser reduzidos, é possível entender que que: a referência 3 corresponde ao índice 3 no “array”, que quer dizer “function”. A referência de número 4 é “stack_messages”. Isso é só pra começar.

Note algo interessante: palavras que foram utilizadas várias vezes no código, como var, message e i só aparecem uma vez no array. Elas possuem as referências 2, 8 e 1, respectivamente.

‘3 4(a){2 1=0;5(1=0;1<a.6();1++){2 b=”7 8 #”+1+”: “+a[1]”;9(b)}}’

Trocando de 1 a 7 caracteres por 1 só. Inteligente demais, não?

* Não entendi o porque da palavra message ter duas referências, 8 e b, mas assim que descobrir, atualizo o post.

Não vou me aprofundar nos detalhes do algoritmo de descompressão do Packer, mas por cima é possível notar que:

– È um algoritmo iterativo: o único código que roda imediatamente é ele e o código desenvolvido por você não vai rodar até que ele tenha iterado o array de palavras úteis e transformado o código traduzido em código válido para ser interpretado com eval(). A velocidade com a qual ele roda depende bastante da velocidade da máquina e do browser de seu cliente.
– Não é bom para códigos pequenos. O overhead causado pela função de descompressão não compensa o trabalho da compressão.
– Concatene todos os arquivos JS que sua página necessita através de um pipeliner antes de comprimir com o Packer. Isso faz com que o tamanho do array de palavras úteis seja menor e centralizado. No caso de frameworks e bibliotecas de terceiros, escolha as versões non-minified e non-packed. O ideal é não judiar do forçar o browser do seu cliente a executar o descompactador do Packer mais de uma vez, seja em cada arquivo, seja em “compressão em cima de compressão”, como um código packed passando novamente pelo Packer.

Nesse ponto, meu código (XRegExp + minha aplicação) já está “minified” com o Packer, mas ainda está pesado para o cenário de milhões de requests. Vou explicar na parte 2 como o servidor foi otimizado para servir arquivos com compressão.

%d blogueiros gostam disto: