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

Postado em 28. abr, 2011 por Sérgio Lopes em Web Design

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.

Sérgio Lopes ()

Mais sobre o autor

Tags: , , ,

29 Respostas para “Otimizações na Web e o carregamento assíncrono”

  1. Rinaldi Fonseca

    28. abr, 2011

    Ótimo Post! Parabéns!

  2. Lucas Lima de Souza

    28. abr, 2011

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

  3. Wolmir

    28. abr, 2011

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

  4. MayogaX

    28. abr, 2011

    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. abr, 2011

    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. abr, 2011

    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. mai, 2011

    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. mai, 2011

    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. mai, 2011

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

  10. arthur

    04. mai, 2011

    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. mai, 2011

    É o WebPageTest.org, muito boa ferramenta!

  12. Edinei

    05. mai, 2011

    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. mai, 2011

    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. mai, 2011

    Explicado =D Obrigado pelos esclarecimentos!!!

  15. Adriano Patrick Cunha

    12. mai, 2011

    muito bacana o post

  16. Jean C Becker

    12. mai, 2011

    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. mai, 2011

    Que post fantástico, parabéns!

  18. Cleyson Lago

    26. mai, 2011

    Bom, muito bom.

  19. Mayra

    29. jan, 2013

    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. fev, 2013

    Pergunta noob:

    Onde coloca isso

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

    ?

  21. Eli Morais

    28. mar, 2013

    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. abr, 2013

    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. abr, 2013

    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. mai, 2013

    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

Trackbacks/Pingbacks

  1. links for 2011-04-30 « pabloidz - abril 30, 2011

    [...] Otimizações na Web e o carregamento assíncrono | blog.caelum.com.br (tags: webdesign) [...]

  2. Prepare-se para o segundo Caelum Day in Rio! | blog.caelum.com.br - maio 3, 2011

    [...] de 5 anos de XP e Scrum, para onde ir?” e Sérgio Lopes que passa as últimas dicas para web site otimizadas. Haverá palestras sobre Android, JRuby, Clojure e Java [...]

  3. Por uma Web mais rápida: 26 técnicas de otimização de Sites | blog.caelum.com.br - setembro 12, 2011

    [...] Uma das técnicas avançadas mais discutidas ultimamente – e das mais eficazes – é o uso de carregamento assíncrono de componentes da página. Isso evita bloqueios na renderização e faz a página ficar mais responsiva. Fiz um post só sobre esse assunto aqui no blog. [...]

  4. Como otimizar as requisições do seu site | Webgoal - março 27, 2012

    [...] Otimizações na web e o carregamento assíncrono [...]

  5. Dividindo seu sistema web através da própria web | blog.caelum.com.br - setembro 5, 2012

    [...] significa que não há cuidados a serem tomados, como o tempo de carregamento de uma página, o número de requests simultâneos e diversas outras otimizações da web que são [...]

Deixar uma Resposta