Ajax sem Templates? Paginação sem PushState? Você está fazendo isso errado!

Algumas coisas de fato me revoltam na vida, entre elas quando alguns profissionais fazem algumas coisas nas coxas sem todo o potencial por assim dizer.

Um dos sites que me motivaram a escrever este artigo foi o site legendas.tv. Para quem não conhece o Legendas.tv é um site que distribui arquivos de legendas para filmes, e sua interface faz uso massivo de Ajax. O problema que eu tenho com este site é que ele é mantido pela comunidade (recebe doações e mais recentemente está trabalhando com um sistema de usuários VIPs), possui milhares de acesso por dia e ainda assim não é otimizado para trabalhar da forma que se propõe, vou descrever abaixo os pontos que me levam a dizer isso.

Intro ao AJAX

O principio do AJAX de forma simplificada é permitir a uma página Web fazer uma requisição a um servidor e receber uma resposta sem que seja necessário recarregar a página, vemos quando usamos clientes de email como o GMAIL por exemplo, no qual clicamos em algum link e apenas uma parte da janela é atualizada e não ela toda.

O AJAX é uma sigla para “Asynchronous Javascript and XML”, que propõe o uso assincrono do Javascript e XML, isso quer dizer que você pode disparar uma requisição Javascript, continuar usando a página e quando a resposta chegar a página é atualizada, caso fosse sincrona seria necessário aguardar a requisição retornar uma resposta para continuar.

Os navegadores permitem que você receba qualquer coisa como resposta, seja XML, JSON, HTML entre outros e ai é onde vive o problema, inicialmente se usava XML porque ele é um formato para descrever dados, sem formatação.

Mais tarde veio o JSON, JSON é uma alternativa ao XML ele é mais leve e nativo do Javascript, o que tornou ele o formato preferido de algumas organizações para este tipo de requisição.

O problema começou quando as pessoas viram que era possível trazer HTML como resposta, como vamos ver abaixo.

AJAX, HTML e Templates.

Como sabemos quase todas as páginas que vemos na internet estão em HTML, mesmo que este tenha sido gerado por outra técnologia, para ganhar tempo algumas pessoas preferem usar o HTML como resposta, como é o caso do Legendas.tv.

Quando clicamos para trocar de página no legendas.tv eis o fluxo de informações que ocorre.

  1. A página envia uma requisição para o servidor pedindo a próxima página.
  2. O servidor procura no banco de dados as informações que são necessárias para a próxima página.
  3. O servidor recebe as informações e inicia processo de formatação HTML para o conteúdo.
  4. Servidor termina o HTML do conteúdo e envia como resposta para a página que enviou a requisição.
  5. A página atualiza o container com o HTML recebido.

Certo, até ai algumas pessoas podem não ver problemas, contudo temos alguns, entre eles:

  • O servidor está fazendo o processo de formatação, que é um processamento desnecessário que pode ser feito pelo Javascript no navegador do cliente, sendo assim está disperdiçando processamento da máquina, ou seja, pode processar menos requisições ao mesmo tempo.
  • O HTML que retorna é maior em tamanho do que um JSON ou XML, uma vez que carrega TAGs para formatação, ou seja, está disperdiçando banda do servidor, neste caso está sendo cobrado mais dinheiro por enviar mais dados, fora que se existir um limite de banda, eles diminuem o número de requisições que podem ser atendidas.

Vamos a um exemplo real para ver o disperdício que existe:

<section class="wrapper js_tabs style_1">
	    <nav>
	    	<a href="/util/carrega_destaques/todos" class="active ajax">TODOS OS DESTAQUES</a><a href="/util/carrega_destaques/series" class="ajax">SÉRIES TV</a><a href="/util/carrega_destaques/filmes" class="ajax">FILMES</a><a href="/util/carrega_destaques/cartoons" class="ajax">CARTOONS</a>	    </nav>

<div class="galery">

<div class="clearfix">
	    	<!--nocache:001-->

			<div class="film">
	    			<img src="/img/poster/140x207/legendas_tv_20131216182323.png" width="140" height="207" alt="">
    				<h3>
    					<a href="/download/534ae79008400/Sirens_US/Sirens_2014_S01E05_HDTV_x264_EXCELLENCE_AFG_EVO_REMARKABLE_mSD">Sirens (US)<span>HDTV | 720p</span><span>S01E05</span></a>
    				</h3>
    				<p>Downloads: <span>371</span></p>
    				<p>Por: <span>
    					<a href="/usuario/OutcastSubs">OutcastSubs</a></span>
    				</p>
    				<p>Em: 13/04/2014 16h37</p>
    				<span class="bt_seta_download">
    					<a href="/download/534ae79008400/Sirens_US/Sirens_2014_S01E05_HDTV_x264_EXCELLENCE_AFG_EVO_REMARKABLE_mSD" class="seta"></a>
    					<a href="/download/534ae79008400/Sirens_US/Sirens_2014_S01E05_HDTV_x264_EXCELLENCE_AFG_EVO_REMARKABLE_mSD" class="texto">DOWNLOAD</a>
    				</span>
	    		</div>

			<div class="film">
				<img src="/img/poster/140x207/legendas_tv_20131012183040.jpg" width="140" height="207" alt="">
				<img src="/img/equipe/132x18/legendas_tv_20130828221128.jpg" width="130" height="18" class="img_equipe" alt="">
				<h3>
					<a href="/download/534ae5bc57da1/Reign/Reign_S01E17_HDTV_x264_2HD_FUM_EVO_mSD_2HD_BS">Reign<span>HDTV | 720p | 1080p</span><span>S01E17</span></a>
				</h3>
				<p>Downloads: <span>1828</span></p>
				<p>Por: <span><a href="/usuario/MysticSubs">MysticSubs</a></span></p>
				<p>Em: 13/04/2014 16h30</p>
				<span class="bt_seta_download">
					<a href="/download/534ae5bc57da1/Reign/Reign_S01E17_HDTV_x264_2HD_FUM_EVO_mSD_2HD_BS" class="seta"></a><a href="/download/534ae5bc57da1/Reign/Reign_S01E17_HDTV_x264_2HD_FUM_EVO_mSD_2HD_BS" class="texto">DOWNLOAD</a>
				</span>
			</div>

			<!-- Mais uns 10 que eu tirei para não ficar grande -->			

			<section class="pagination_wrapper">

<div class="pagination">
					<button class="ajax left" data-href="/util/carrega_destaques/todos/page:1" rel="prev"></button><button class="ajax" data-href="/util/carrega_destaques/todos/page:1">1</button><button class="active">2</button><button class="ajax" data-href="/util/carrega_destaques/todos/page:3">3</button><button class="ajax" data-href="/util/carrega_destaques/todos/page:4">4</button><button class="ajax" data-href="/util/carrega_destaques/todos/page:5">5</button><button class="ajax" data-href="/util/carrega_destaques/todos/page:6">6</button><button class="ajax" data-href="/util/carrega_destaques/todos/page:7">7</button><button class="ajax" data-href="/util/carrega_destaques/todos/page:8">8</button><button class="ajax" data-href="/util/carrega_destaques/todos/page:9">9</button><button class="ajax right" data-href="/util/carrega_destaques/todos/page:3" rel="next"></button>			    </div>
			</section>

		</div>
	</section>

Agora se fossemos usar JSON para resposta teriamos algo como:

{
    "page" : 2,
    "totalPages" : 9
    "legendas" : [
        {
            "nome" : "Sirens (US)",
            "releases" : "HDTV | 720p",
            "episode" : "S01E05",
            "downloads" : "371",
            "enviado" : "13/04/2014 16h37",
            "poster" : "/img/poster/140x207/legendas_tv_20131216182323.png",
            "permalink" : "/download/534ae79008400/Sirens_US/Sirens_2014_S01E05_HDTV_x264_EXCELLENCE_AFG_EVO_REMARKABLE_mSD",
            "usuario" : {
                "nome" : "OutcastSubs",
                "poster" : "",
                "permalink" : "/usuario/OutcastSubs"
            }
        },
        {
            "nome" : "Reign",
            "releases" : "HDTV | 720p | 1080p",
            "episode" : "S01E17",
            "downloads" : "1828",
            "enviado" : "13/04/2014 16h30",
            "poster" : "/img/poster/140x207/legendas_tv_20131012183040.jpg",
            "permalink" : "/download/534ae5bc57da1/Reign/Reign_S01E17_HDTV_x264_2HD_FUM_EVO_mSD_2HD_BS",
            "usuario" : {
                "nome" : "MysticSubs",
                "poster" : "/img/equipe/132x18/legendas_tv_20130828221128.jpg",
                "permalink" : "/usuario/MysticSubs"
            }
        },
        // mais uns 10 que eu omiti para não ficar muito longo
    ]
}

Isso ainda podemos otimizar o JSON fazendo as seguintes mudanças

  • removendo o atributo permalink do usuário uma vez que isso nada mais é que “/usuario/” concatenado com o nome do usuário, podemos remover também as
  • removendo strings dos links das urls e passando para arquivos uma vez que:
    • Todos os posters dos filmes estão no endereço /img/poster/140×207/
    • Todas as imagens de equipes estão no endereço /img/equipe/132×18/
    • Todos os downloads estão no endereço /download/

Ficando de:


        {
            "nome" : "The Neighbors",
            "releases" : "HDTV | 720p",
            "episode" : "S02E21",
            "downloads" : "550",
            "enviado" : "13/04/2014 16h00",
            "poster" : "/img/poster/140x207/legendas_tv_20130923202022.jpg",
            "permalink" : "/download/534adeb63c86b/The_Neighbors/The_Neighbors_2012_S02E21_HDTV_x264_LOL_AFG_DIMENSION_mSD",
            "usuario" : {
                "nome" : "SuBMakerS",
                "poster" : "/img/equipe/132x18/Submakers.gif",
                "permalink" : "/usuario/SuBMakerS"
            }
        },

Para:


        {
            "nome" : "The Neighbors",
            "releases" : "HDTV | 720p",
            "episode" : "S02E21",
            "downloads" : "550",
            "enviado" : "13/04/2014 16h00",
            "poster" : "legendas_tv_20130923202022.jpg",
            "permalink" : "534adeb63c86b/The_Neighbors/The_Neighbors_2012_S02E21_HDTV_x264_LOL_AFG_DIMENSION_mSD",
            "usuario" : {
                "nome" : "SuBMakerS",
                "poster" : "Submakers.gif",
            }
        },

Com isso conseguimos:

  • Tamanho médio da resposta HTML: 10kb
  • Tamanho médio da resposta JSON: 05kb
  • Tamanho médio da resposta JSON Otmizada: 04kb

O que representa um corte de 50% nas informações enviadas pelo servidor, a cada 10000 acessos que são aproximadamente 3 mil usuários acessando entre 3 e 4 páginas (mudanças de página) isso representa o seguinte:

  • HTML: 12mb
  • JSON: 06mb
  • JSON Otimizado: 04.8mb

De forma gráfica temos:

Gráfico dos formatos demonstrando os dados acima de transferencia.

Para isso funcionar seria necessário adicionar uma template como o handlebars ou mustache e lógica para ajustar o link da paginação.

Se fossemos usar o mustache por exemplo, seria adicionado ao html da página algo como:

Considerando JQuery e Mustache carregados.

<script id="template" type="x-tmpl-mustache">
  {{#legendas}}
            <div class="film">
                <img src="/img/poster/140x207/{{poster}}" width="140" height="207" alt="">
                <img src="/img/equipe/132x18/{{usuario.poster}}" width="130" height="18" class="img_equipe" alt="">
                <h3>
                    <a href="/download/{{permalink}}">{{nome}}<span>{{release}}</span><span>{{episode}}</span></a>
                </h3>
                <p>Downloads: <span>{{downloads}}</span></p>
                <p>Por: <span><a href="/usuario/{{usuario.nome}}">{{usuario.nome}}</a></span></p>
                <p>Em: {{enviado}}</p>
                <span class="bt_seta_download">
                    <a href="/download/{{permalink}}" class="seta"></a><a href="/download/{{permalink}}" class="texto">DOWNLOAD</a>
                </span>
            </div>
  {{/legendas}}
</script>
<script>
  // Carrega conteúdo da template em variavel.
  var template = $('#template').html();
  // ... aqui vem a chamada ajax, passamos o callback para ser executado em caso de sucesso!
  chamadaAjax(function(data) {
    var resposta = JSON.parse(data);
    var htmlContainerRendered = Mustache.render(template, resposta.legendas);
    $('#pagination_target .galery .clearfix').html(htmlContainerRendered);

    // ai vem script para atualizar links da paginação e outros da interface.

  });

</script>

Feito isso você já otimiza sua aplicação fazendo com que:

  • Seja transferido via banda somente o necessário, sendo inclusive possível cachear as chamadas AJAX com memcache por exemplo.
  • O cliente tenha um cache da template (Cache da página e dos scripts, a template pode ser inserida em um script externo que pode ser cacheado.)
  • O cliente (navegador) faça o trabalho de formatar o conteúdo.

Complementando com PushState

Um dos problemas que temos com esta ideia de ficar fazendo AJAX para carregar outras páginas são as URLs, neste caso voltando ao Legendas.tv quando clicamos para outra página o endereço fica o mesmo e quando clicamos em voltar acabamos por sair do site ao invés de voltar a página anterior.

Até algum tempo atrás não tinhamos muitas opções para contornar este problema quando usavamos AJAX, mas agora já temos, com o HTML5 ganhamos um método de API chamado pushState, da API history.

Este metodo permite que adicionemos ao histórico novos endereços de páginas sem que seja necessário recarregar nossa página conforme descrito na página da MDN sobre Manipulating the browser history.

Desta forma aplicações como o GMAIL, GitHub e outras conseguem trocar o link da URL quando alguém clica em alguma configuração e permite aos seus usários usar os botões dos navegadores de avançar e voltar para mudar a um estado anterior sem recarregar as páginas.

Com isso você ganha alguns benefícios.

  • Suas páginas podem ter URLs únicas que podem ser usadas para fazer favoritos e compartilhar por email, facebook ou twitter (entre outros).
  • Você só carrega a porção que muda da página quando alguém navega em seu site.

Como usar

O history.pushState pode ser usado diretamente via Javascript conforme mostrado no Manipulating the browser history, contudo como alguns navegadores podem ter implementado de forma diferente e alguns mais antigos não suportam você pode usar o history.js que dá uma API única para gerenciar ambos os casos.

Uma estratégia para tomar completo proveito do push state é pensar em forma de componentes, por exemplo, vamos tomar o GMAIL como exemplo, o GMAIL tem a coluna lateral que apresenta sua inbox e se ativado seu Hangout entre outros itens e a coluna principal que apresenta a lista de mensagens, uma mensagem específica ou as configurações, logo em toda a página que for aberta você terá estas duas áreas com 100% de certeza.

  • A coluna lateral será carregada sempre e poderá receber um atributo no componente de caixas de entrada para deixar em destaque a caixa que está sendo usada.
  • O hangouts também terá isso para cada hangout aberto.
  • A coluna principal irá carregar o conteúdo de acordo com sua URL.

O truque aqui é que sua aplicação assim como o GMAIL já deve carregar as templates de cada um dos componentes na hora da abertura, assim como os scripts responsáveis, quando iniciar ele deve carregar a página com estes componentes já abertos, ai quando alguém quiser acionar outro componente, por exemplo, abrir uma mensagem ele carrega apenas a mensagem e sua URL, muda a URL via history.pushState e carrega ela no componente dela na tela.

Do lado do servidor você deve ser capaz de servir dois tipos de requisição, o primeiro é o de página inteira acionado quando um usuário carrega a página pela primeira vez, o segundo é de dados específicos do componente, carregado via AJAX com um cabeçalho diferente.

O Github tem um projeto chamado PAJAX que implementa esta funcionalidade, quando a página é aberta pela primeira vez o servidor recebe uma requisição normal, quando é via AJAX ele recebe um header chamado X-PAJAX, sendo assim, o servidor não envia a página inteira, apenas os dados necessários para a parte pedida.

Nos navegadores antigos é usada uma técnica chamada hash-fallback, no qual é adicionado um # após a url e adicionado o caminho a ser carregado, neste caso o servidor tem que ser preparado para tratar URLs com Hashs para que quando a pessoa acesse a página por ela ele seja capaz de servir a página.

Entendendo as coisas.

Essas duas técnicas passadas aqui hoje são o fundamento para qualquer aplicação web que deseje se aproveitar do cunho assincrono da Web, também pode ser implementada com diversos frameworks javascript e de backend, e ganha força quando usada em conjunto com a arquitetura MVC usada nos frameworks web.

Os frameworks permitem que cada componente seja visto como um controller, as informações como modelos e a apresentação das mesmas como views, sendo assim por exemplo, quando você edita algo no Javascript que tem um model ele chama o método save(), que por sua vez implementa uma chamada save ao servidor que retorna uma resposta específica para o método para que o mesmo atualize a view da aplicação, e assim por diante.

No Rails por exemplo, é possível renderizar a página inteira ou apenas um pedaço dentro do controller consultando os headers, podendo inclusive fazer um helper, no NodeJS com o Express podem ser feitos middlewares que irão tratar as requisições e assim por diante.

Estou ansioso para escutar de vocês. Se tiverem dúvidas ou precisarem que eu explique algo mais afundo por favor, deixem comentários abaixo.

2 Comments

Sim, eu tentei avisar eles antes do Redesign, mas acho que eles não leram o meu post no fórum deles, na época os ganhos eram maiores do que hoje.

Se você reparar no código que eu usei no exemplo você vê que não só é retornado markup, que há alguns que servem apenas para layout como uma div que tem a classe Clearfix (Usada para que eles possam usar floats sem que o container fique com 0 de altura) como eles também tem em alguns pontos classes desnecessárias, um exemplo é no link para download de seta que tem o seguinte código.

<span class="bt_seta_download">
  <a href="/download/534ae5bc57da1/Reign/Reign_S01E17_HDTV_x264_2HD_FUM_EVO_mSD_2HD_BS" class="seta"><a href="/download/534ae5bc57da1/Reign/Reign_S01E17_HDTV_x264_2HD_FUM_EVO_mSD_2HD_BS" class="texto">DOWNLOAD
</span>

Neste código há uma classe no span e uma em cada a, quando uma vez que já havia no span tudo o que eles precisavam fazer para usar como alvo o a era usar o seletor:

span.bt_seta_download > a:first-child e span.bt_seta_download > a:last-child

Onde todos os links que são filhos diretos de um span com a classe bt_seta_download são alvos, pensar que a seta existe pelo menos 12 vezes na página e que haveriam no máximo 2 a 3 declarações no CSS e duas a 3 no JS se houver alguma.

Deixe um comentário para Henrique Cancelar resposta

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.