Otimizações na Web e o carregamento assíncrono

No fim de março, colocamos no ar a nova Home page da Caelum. Com mais conteúdo relevante, integração com nosso Blog, Twitter e Facebook, a página passou a ser um desafio de performance.

Somos bitolados por otimizações Web aqui na Caelum então muitas otimizações já estavam integradas ao Site. A primeira versão, logo após a fase de design e codificação, tirava uma invejável nota 90 no PageSpeed do Google. Mas sua performance não era aceitável para nós. Com um pouco mais de esforço e técnicas mais avançadas, conseguimos o seguinte resultado (antes e depois):

Vídeo Antes e Depois das Otimizações na Home da Caelum.
Veja também quadro a quadro a brutal diferença entre as duas páginas.

Após as modificações, a nota no PageSpeed até subiu um pouco, para 95, mas o impacto para o usuário final é muito maior. Ao preocupar-se com otimizações Web, o uso de ferramentas como o PageSpeed ou YSlow é muito importante, mas ainda mais crucial é executar testes frequentes e analisar os tempos. A ferramenta WebPageTest, usada no vídeo acima, é excelente para esse fim.

O último post aqui do Blog sobre otimização de performance na Web mostou 7 práticas simples e altamente recomendadas para melhorar sensivelmente a performance de qualquer página Web, como usar GZIP, agrupar e comprimir CSS e JavaScript, colocar CSS no topo e JavaScript embaixo da página, entre outras. Mas há muitas outras práticas interessantes, como o carregamento assíncrono de JavaScript e componentes não essenciais à página.

Carregamento assíncrono de JavaScript

Colocar arquivos JavaScript no <head> ou espalhado no meio do HTML é má prática há anos. A boa prática é sempre jogar para antes do </body>, uma das regras principais analisadas pelo YSlow. Mas será que jogar o JavaScript para o fim da página é suficiente?

Muitos navegadores não suportam download em paralelo de arquivos JavaScript quando usamos a tag <script>, independente da posição em que apareça. E não apenas navegadores antigos como IE6 ou 7; Chrome, Opera, Firefox3 e outros engrossam essa lista. Steve Souders, papa de otimizações Web, sugeriu diversas técnicas em seu livro Even faster Web Sites. Começaram a surgir então ideias e bibliotecas com objetivo de carregar JavaScript em paralelo assincronamente, como LABjs, HeadJS e ControlJS, do próprio Steve.

A página da Caelum usa o LABjs, mas independentemente de framework, o importante é carregar assincronamente seus arquivos JavaScript, principalmente se vários arquivos são usados na página. Além do download paralelo, o evento onload dispara mais cedo, executando os callbacks associados a ele e dando uma medida melhor de performance da página.

É preciso tomar cuidado, porém, com a ordem de execução dos scripts caso haja dependências entre eles (muito comum ao usar um framework como JQuery). As ferramentas citadas possuem suporte para manter a ordem de execução. Um outro problema é quando há uso do document.write, algo que é má prática há muito tempo mas infelizmente ainda muito usado.

Usar o LABjs é bastante simples:

// carrega 3 scripts em paralelo mas mantém ordem de execução
$LAB.script('jquery.js').wait()
    .script('plugin.jquery.js').wait()
    .script('app.js');

// carrega e executa outro script em paralelo, com callback
$LAB.script('sem-dependencias.js').wait(function() {
   alert('Callback executando quando script carrega');
});

O HTML5 especificou um novo atributo async na tag <script> mas poucos browsers suportam. O IE possui um atributo proprietário defer há muito tempo, com propósito parecido. Enquanto não há uma solução padrão e portável, o uso de algum framework de carregamento assíncrono é recomendado.

Adiando o carregamento de conteúdo secundário

Além de JavaScripts assíncronos, é possível melhorar bastante a performance deixando para carregar certos componentes mais tarde, apenas quando necessários. A nova home da Caelum tem fotos grandes com chamadas principais rotacionando. É um efeito muito comum atualmente, mas um grande desafio de performance, já que essas imagens costumam ser grandes e pesadas.

Nossa primeira implementação consistia em um HTML simples com tags <img> apontando para cada imagem, um pouco de CSS para mostrar apenas uma imagem por vez e um código JQuery para alternar as imagens com um efeito legal. É uma implementação usada por muitos Sites e plugins do JQuery.

Mas colocar as <img> direto no HTML fazia o navegador carregar todas essas imagens conforme ia lendo o HTML, mesmo que 3 das 4 imagens só fossem aparecer para o usuário muito tempo depois. Era gasto um tempo precioso do carregamento da página, que podia ser usado carregando outros componentes mais essenciais para a renderização inicial, como outras imagens do layout e scripts.

A solução foi carregar assincronamente, via JavaScript, as imagens secundárias, deixando inicialmente apenas a primeira imagem com HTML normal. Usamos os data attributes do HTML5 para criar atributos próprios no HTML que referenciam os endereços das imagens secundárias:

<ul>
	<li>
		<p>Conheça os cursos de Java da Caelum</p>
		<img alt="Cursos Java" src="banner_01.jpg" width="960" height="330" />
	</li>
	<li data-img-src="banner_02.jpg">
		<p>Veja os projetos da Caelum</p>
	</li>
	<li data-img-src="banner_03.jpg">
		<p>Baixe as apostilas gratuitas</p>        
	</li>
</ul>

Repare como apenas o primeiro banner possui a <img> direto no HTML. Os demais apenas declaram os caminhos em atributos data- que não são interpretados pelo navegador. Uma função JavaScript pode, então, ler esses valores e gerar as tags <img> de forma assíncrona, depois que a página foi carregada:

$(function() {
	setTimeout(function(){
		$('li[data-img-src]').each(function(){
			var src = $(this).attr('data-img-src');
			$('<img>').attr('src', src).appendTo('ul');
		});
	}, 600);
});

Usando JQuery, selecionamos todos os <li> que possuem o atributo data-img-src. Criamos, então, um novo elemento <img> com o src apontando para o endereço da imagem. Repare que tentamos adiar esse carregamento o máximo possível, já que a segunda imagem só aparecerá para o usuário depois de vários segundos. No código acima, agendamos o script para rodar 600 milissegundos após o carregamento da página.

Observe no gráfico de conexões HTTP ao longo do tempo como o primeiro banner é carregado no início junto com o restante da página e os demais são carregados bem depois:

Uma preocupação possível com essa prática é com usuários com JavaScript desabilitado ou navegadores limitados. É preciso pensar bem nesse caso e oferecer uma boa experiência para o usuário em todas as situações. Mas as imagens rotativas dependem de JavaScript para funcionar; logo, mesmo sem o carregamento assíncrono das imagens secundárias, o efeito não funcionaria e apenas a primeira imagem (em HTML e CSS puros) seria mostrada. Portanto, não há impacto para o usuário em usar a solução JavaScript para carregamento das imagens. Um impacto possível é que os buscadores não enxergam mais as imagens secundárias e, portanto, estas não serão indexadas. Não é um problema no nosso caso, mas pode ser um detalhe importante em outros cenários.

Widgets externos assincronamente

Outro bom exemplo para carregamento assíncrono é dos widgets do Facebook, usados na home da Caelum com um Like Button no topo e um Like Box no corpo da página. Onipresentes hoje na Web, esses widgets são importados na página com um <iframe>. O <iframe> tem a vantagem de carregar paralelamente com a página, mas ainda trava o onload da página até que acabe de carregar – e os widgets do Facebook são gigantescos, com mais de 50 requests, 250 KB e 5 segundos para carregar mesmo em navegadores modernos.

Steve Souders mostrou que a tag <iframe> é o elemento mais caro a ser criado no DOM, ordens de magnitude mais lento, mesmo vazio. Mas o principal problema é o onload da nossa página passar a depender do onload do widget, que é grande e lento. Devemos otimizar o tempo que o onload dispara, pois isso dispara os callbacks de onload (bastante comuns) e porque o indicador de carregamento do navegador para de girar após o onload (dando a sensação de rapidez pro usuário).

A melhor estratégia é carregar o <iframe> após o onload via JavaScript, ainda mais se não é algo crítico, como o widget do Facebook. O código é bastante simples:

$(window).load(function(){
	$('#facebook_like_box')
		.html('<iframe src="https://www.facebook.com/plugin...></iframe>');
});

Esse carregamento assíncrono do Facebook foi o responsável pela maior parte da otimização final mostrada no vídeo do início do post. Repare no gráfico a seguir como o carregamento todo dos widgets é feito apenas após o onload e, apesar de grande e lento, não afeta a performance do restante da página:

Como o widget demora bastante para carregar, o resto da página (já bastante otimizada) aparecerá rapidamente mas o widget bem depois. Para minimizar esse efeito, o header da página da Caelum carrega primeiro um botão like de mentira, com uma imagem simples copiando o visual do widget do Facebook. Quando o widget acaba de carregar, ele é inserido no lugar dessa imagem falsa com precisão, a ponto de ser quase imperceptível para o usuário. Se o usuário clicar na imagem falsa antes do widget carregar, será levado para o Facebook da Caelum; mesmo comportamento se o JavaScript estiver desabilitado. É um truque simples que traz a sensação de alta performance ao carregar a imagem falsa logo, apesar de ser tudo assíncrono e demorado.

Mais ideias

Há muitas outras ideias para carregamento assíncrono. Os desenvolvedores do SlideShare, por exemplo, recentemente mostraram o impacto de carregar as imagens lazy apenas quando o usuário fizer scroll para vê-las. Há inclusive quem defenda uma nova métrica, o Above The Fold (AFT) Time apresentado na Velocity Online 2011 mês passado, que leve em conta apenas os objetos necessários para a primeira impressão do usuário.

O importante é otimizar o tempo de carregamento inicial da página, adiando tudo aquilo que não é essencial para o usuário visualizar de primeiro.

33 Comentários

  1. Rinaldi Fonseca 28/04/2011 at 10:34 #

    Ótimo Post! Parabéns!

  2. Lucas Lima de Souza 28/04/2011 at 14:15 #

    Excelente Post, belas práticas que muitas vezes não são consideradas.

  3. Wolmir 28/04/2011 at 14:33 #

    Parabéns pelo post, estava procurando por isso a muito tempo…. cara muito bom…

  4. MayogaX 28/04/2011 at 15:02 #

    Nossa, parabéns pelo post.
    Muito bom no conteúdo!

    Eu fiquei arrepiada de ver o micro vídeo com a diferença entre o antes e o depois… nossa, muita diferença.

  5. Leandro 28/04/2011 at 15:35 #

    Grande conteúdo! Obrigado por partilhar valiosa informação. Parabéns. E ficou claro que vocês são ensandecidos por otimização.

  6. Regis 28/04/2011 at 16:02 #

    Bem legal o post, e varios pontos onde voce disse que é “é má prática há anos” mais que ainda acabamos usando no dia-a-dia.

  7. Ju Nogueira 02/05/2011 at 21:03 #

    dicas valiosas, obrigada!
    e muito legal o vídeo mostrando que os métodos de otimização sugeridos funcionam muito bem e fazem uma grande diferença!

  8. Altieres 03/05/2011 at 09:55 #

    Post com ótimo conteúdo e em momento oportuno!
    Estamos trabalhando na otimização do nosso produto de controle financeiro (www.granatum.com.br) e com certeza iremos aplicar essas técnicas!

  9. Luiz Roberto Freitas 03/05/2011 at 13:48 #

    Excelente post!! As dicas são fantásticas!

  10. arthur 04/05/2011 at 18:02 #

    Só uma perguntinha besta: Qual programa vocês utilizaram para ver o que estava sendo carregado na requisição? Vocês mostram uma imagem com o ponto exato onde é executado o evento onLoad. A própria ferramenta mostra essa informação??

  11. Sérgio Lopes 04/05/2011 at 18:03 #

    É o WebPageTest.org, muito boa ferramenta!

  12. Edinei 05/05/2011 at 12:45 #

    Muito bom o post!! Embora principiante no quesito performance, interesso bastante sobre isso.

    Mas algumas dúvidas:

    - Onde mostra o LABjs (carrega 3 scripts em paralelo mas mantém ordem de execução), o fato de manter/aguardar a execução do js não torna o carregamento síncrono?

    - Onde diz sobre o widget do facebook (a melhor estratégia é carregar o após o onload via JavaScript), a performace ocorreu pela mudança de ordem após o onload, ou seja, houve carregamento assíncrono neste caso?

    Obrigado e abs

  13. Sérgio Lopes 05/05/2011 at 19:45 #

    Oi Edinei! Respondendo suas perguntas:

    - Você tem razão: ao ter scripts com dependência entre si, um bloqueia a execução do outro. Mas se os scripts forem independentes, a execução é em paralelo (mais rápido). Mas a questão de ser assíncrono é que os scripts (mesmo com dependências) não travam mais a renderização da página e de outros recursos (imagens, css etc). A chamada normal com a tag ‘script’ (síncrona) trava a renderização. Falamos então que o LABjs torna os scripts assíncronos com relação à página que pode ser renderizada normalmente.

    Aliás, o ControlJS leva essa coisa do assíncrono um passo adiante. Ele separa o download da execução do script, e consegue fazer todos os downloads em paralelo mesmo quando a execução tem que ser feita em certa ordem.

    - Dos iframes, a grande questão é que eles bloqueiam o evento de onload da página (e outras renderizações). Como muitos scripts usam o onload para rodar algum callback importante, os iframes normais acabam segurando esses callbacks até o momento de serem completamente carregados. Por isso falamos que o iframe normal é síncrono. Ao inserir o iframe via JS, você não bloqueia o onload e o carregamento do seu iframe ocorre em paralelo inclusive com os seus callbacks de onload.

    Mas concordo com você que o maior ganho de performance para o usuário na prática foi mais com relação a deixar o widget para depois. Assim o resto da página (mais importante) carrega mais rápido e o widget (menos importante) só aparece bem depois. É mais um truque de “parecer mais rápido” para o usuário do que realmente executar um monte de coisa em paralelo.

    Abraços

  14. Edinei 06/05/2011 at 11:12 #

    Explicado =D Obrigado pelos esclarecimentos!!!

  15. Adriano Patrick Cunha 12/05/2011 at 11:12 #

    muito bacana o post

  16. Jean C Becker 12/05/2011 at 11:21 #

    Excelente post, estou otimizando o desempenho do meu site e tambem ja consegui o ranking 90 no page speed, minha meta agora é 95!

  17. Rodrigo 12/05/2011 at 17:07 #

    Que post fantástico, parabéns!

  18. Cleyson Lago 26/05/2011 at 10:16 #

    Bom, muito bom.

  19. Mayra 29/01/2013 at 18:24 #

    Da para ver que a coisa funciona, mas para pessoas noobs como eu, colocar essas funções dentro do wordpress é complicado. A não ser que estejam direcionadas ex: na pasta functions. php coloque isso, no head coloque isso, se não for assim não dá. Mas parabéns pelo post.

  20. Doutor Gueimiplei 16/02/2013 at 12:43 #

    Pergunta noob:

    Onde coloca isso

    ” $(window).load(function(){
    $(‘#facebook_like_box’)
    .html(”);
    }); ”

    ?

  21. Eli Morais 28/03/2013 at 06:03 #

    Parabéns.
    Realmente a prátcia de técnicas como esta, fazem a diferença na audiência, e cnsequentemente, na rentabilidade financeira de um site.

  22. Guilherme Velloso 10/04/2013 at 14:41 #

    Sérgio Lopes, parabéns pelo post fantástico. Mas ainda tenho uma duvida e de cara peço desculpas por ser tão leigo..hehe

    Uso um aplicativo no wordpress que se chama performance que pega todos os arquivo .js, cria um novo js com todos os outros dentro sem espaços e etc. Então ele deleta todas aquelas chamadas de js e cria uma unica chamada do tipo 3425jkljlsdf828.js

    Como eu poderia usar o Async neste caso? Acredito que teria que editar o app…ou teria uma forma melhor e mais dinâmica de se fazer isso?

    Att,
    Guilherme Velloso

  23. Alison 30/04/2013 at 17:51 #

    olá sou novato em questão de otimização , e gostaria de saber como aplicar esta tática do “Adiando o carregamento de conteúdo secundário ”
    em um

    como eu faria na function para indicar o nome que esta dentro do id ? teria como ?

  24. Rubens 08/05/2013 at 01:38 #

    Olá Sergio, tudo bem?

    Gostaria de saber se existe alguma opção de utilizar o adiamento do carregamento de conteúdo secundário porém sem utilizar o …
    no codigo que tenho tenho uma

    Quando coloco essa ele desconfigura tudo um banner (slider) que usa jQuery…

    Obrigado pela ajuda

  25. Lucas Maus 16/08/2013 at 20:50 #

    Muito bom o post, Sérgio Lopes.

    Tirei as dúvidas que eu tinha sobre carregamento assíncrono.

    Muito obrigado pelas dicas.

  26. Thiago Souza 06/09/2013 at 18:30 #

    Senhores, primeiro parabéns pelo artigo. Eu estou implementando isso num sistema novo e gostaria de compartilhar uma ideia/duvida:

    no lugar usar uma lista com o caminho da imagem não seria melhor usar um link? Isso em teoria faria o Google saber da existencia da imagem e não atrapalharia o SEO da página.

    Assim:
    Nome da Imagem

    Pra trocar por uma imagem depois que a página estiver carregada.

    Abs a todos!

  27. Jackson Rubem 27/10/2013 at 17:22 #

    Olá Sergio,
    Parabéns pelo seu texto. Bem abrangente e excelente explicação.
    No entanto, gostaria de saber como aplicar sua orientação no WORDPRESS. Aprendi a teoria, neste post, mas preciso praticá-la no WORDPRESS. É possível?

  28. Douglas 01/04/2014 at 11:52 #

    Excelente. Parabéns pelas dicas.

Deixe uma resposta