7 Princípios de aplicativos web modernos - parte I
Este artigo foi escrito originalmente pelo desenvolvedor Guillermo Raunch, baseado em sua palestra no BrasilJS em agosto de 2014. Apresentamos aqui uma tradução do artigo original para aqueles que não dominam a língua inglesa.
7 Principles of Rich Web Applications
Irei introduzir 7 princípios acionáveis para websites que desejem fazer uso de JavaScript para controlar sua UI (do inglês: User Interface). Eles são o resultado da minha experiência como desenvolvedor web, mas também como usuário da WWW há muito tempo.
JavaScript inegavelmente se tornou uma ferramenta indispensável para desenvolvedores de interface visual. Seu uso está se expandindo para outras áreas como servidores e microcontroladores. É a linguagem escolhida para introduzir conceitos da ciência da computação por universidades de prestígio.
Ainda assim, muitas questões sobre seu uso exato na web continuam a ser um mistério, mesmo para muitos autores de framework e library.
- JavaScript deve ser usado para substituir funções de browser como histórico, navegação e rendering de páginas?
- O backend está morrendo? Devo renderizar HTML?
- Single Page Applications (SPAs) são o novo futuro?
- JS deve aumentar páginas para websites, mas renderizar páginas em web apps?
- Técnicas como PJAX ou TurboLinks devem ser usadas?
- Qual a diferença exata entre um website e uma aplicação web? Deve existir alguma?
O que se segue é minha tentativa de resposta a estas questões. Minha abordagem é examinar o uso de JavaScript exclusivamente do foco da experiência do usuário (UX, do inglês "User Experience"). Em particular, coloco um grande foco na idéia de minimizar o tempo levado pelo usuário para receber os dados nos quais tem interesse. Iniciando pelos fundamentais de networking até uma previsão para o futuro.
- Páginas renderizadas no servidor não são opcionais
- Aja imediatamente no input de usuário
- Reaja à mudanças de dados
- Controle a troca de dados com o servidor
- Não mude a história, faça melhorias
- Atualizações de push code
- Preveja o comportamento
1. Páginas renderizadas no servidor não são opcionais
tl;Dr Renderizar no servidor não é uma questão de SEO, e sim de performance. Considere as idas e vindas adicionais para receber scripts, estilos, e pedidos subsequentes de API. No futuro, considere PUSH HTTP 2.0 de recursos.
A primeira coisa que devo apontar é uma comum falsa dicotomia. Aquela de "apps renderizados pelo servidor vs apps single-page". Se quisermos otimizar para as melhores experiências de usuário e performance possíveis, desistir de um ou de outro nunca é uma boa idéia.
Os motivos são bem óbvios. O meio pelo qual páginas são transmitidas, a internet, possui um limite de velocidade teórico. Isto já foi memorávelmente ilustrado no famoso ensaio/discurso "It's the latency, stupid" de Stuart Cheshire:
- A distância entre Stanford e Boston é de 4310km.
- A velocidade da luz no vácuo é de 300 x 10^6 m/s.
- A velocidade da luz na fibra é cerca de 60% da velocidade da luz no vácuo.
- A velocidade da luz na fibra é 300 x 10^6 m/s * 0,66 = 200 x 10^6 m/s.
- O delay one-way para Boston é 4320km / 200 x 10^6 m/s = 21,6ms.
- O delay de uma rodada para Boston e de volta é 43,2ms.
- O tempo de ping de Stanford até Boston na Internet atual é de aproximadamente 85ms (...)
- Portanto: o hardware da Internet pode atualmente atingir um fator de dois da velocidade da luz.
O delay de uma rodada de 85ms entre Boston e Stanford certamente melhorará com o tempo e suas próprias experiências atuais podem já mostrar isto. Mas é importante notar que existe um mínimo, em teoria, de 50ms entre as duas costas.
A capacidade de bandwidth da conexão de seus usuários pode melhorar de forma notável, uma vez que de forma constante já acontece, mas a latência não vai mudar muito. Isso significa que minimizando o número de idas e vindas você faz com que mostrar informação na página seja essencial para uma boa experiência de usuário e capacidade de resposta.
Isto se torna particularmente relevante de se apontar considerando o aumento de aplicações JavaScript que não possuem marcações além de tags <script> e <link> ao lado de um <body> vazio. Essa classe de aplicação recebeu o nome de "Single Page Application" ou "SPA". Como o próprio nome diz, há apenas uma página que o servidor retorna consistentemente, e o resto é determinado pelo side code do cliente.
Considere o cenário em que o usuário navega para http://app.com/orders/ ao clicar num link ou digitar a URL. Neste momento em que seu aplicativo recebe e processa o pedido, já existem informações importantes sobre o que irá ser exibido na página. Ele pode, por exemplo, fazer pre-fetch dos pedidos da database e incluí-los na resposta. No caso da maior parte dos SPAs, uma página em branco e uma tag <script> é devolvida e outra rodada será feita para receber o conteúdo dos scripts. Para que então uma outra rodada possa ser feita para receber os dados necessários para renderização.
Análise do HTML enviado pelo servidor para cada página de um SPA lá fora
Neste ponto, muitos desenvolvedores conscientemente aceitam este fato porque eles se certificam de que estes saltos extras de rede acontecem apenas uma vez para seus usuários enviando os cabeçalhos de cache adequados nas respostas de script e stylesheet. O consenso geral é que este é um fato aceitável porque uma vez que o pacote for carregado, você pode então lidar com a maior parte da interação de usuário (como transição para outras páginas) sem requisitar páginas adicionais ou scripts.
Ainda assim, mesmo na presença de um cache, há uma penalidade de performance quando se considera análise de script e o tempo de avaliação. O artigo "Is jQuery Too Big For Mobile?" descreve como mesmo para apenas jQuery isto pode estar na casa de centenas de milisegundos para certos navegadores móveis.
E o que é pior, geralmente nenhum tipo de feedback é dado para o usuário enquanto os scripts estão carregando. Isto resulta em uma página em branco sendo exibida que de repente entra em transição para uma página totalmente carregada.
Mais importante, às vezes nos esquecemos que o transporte predominante atual de dados na internet (TCP) se inicia lentamente. Isso basicamente garante que a maior parte dos pacotes de script não serão buscados em apenas uma rodada, tornando a situação descrita acima ainda pior.
Uma conexão TCP se inicia com uma rodada inicial. Se você estiver usando SSL, que é algo importante para que se haja segurança na entrega do script, duas rodadas adicionais são usadas (apenas se o cliente estiver retomando uma sessão). Apenas então o servidor pode iniciar enviando dados, mas como se vê, o faz lentamente e de forma incremental.
Um mecanismo de controle de congestão chamado início lento é criado no protocolo TCP para enviar dados em um crescente número de segmentos. Isto têm duas implicações sérias para SPAs:
1. Grandes scripts levam muito mais tempo para baixar do que parecem. Como foi explicado no livro "High Performance Browser Networking" de Ilya Grigorik, leva "quatro rodadas (...) e centenas de milisegundos de latência para alcançar 64KB de rendimento entre o cliente e o servidor". Neste exemplo, considerando uma ótima conexão com a internet entre Londres e Nova York, leva 225ms antes do TCP ser capaz de alcançar o tamanho máximo do pacote.
2. Desde que esta regra se aplica também para o download inicial da página, ela faz com que o conteúdo inicial que vem renderizado com a página seja muito mais importante. Como Paul Irish conclui em sua apresentação "Delivering the Goods", os primeiros 14kb são crucialmente importantes. Esta é uma ilustração útil da quantidade de dados que o servidor pode enviar em cada rodada em relação ao tempo:
Quantos KB um servidor pode enviar por cada fase da conexão por segmentos
Websites que possuam conteúdo (mesmo que seja apenas o layout básico sem dados) dentro de sua janela parecem ter grande capacidade de resposta. Na verdade, para muitos autores de aplicações server-side rápidas, Javascript é algo considerado desnecessário ou para ser usado esporadicamente. Este viés ainda é reforçado se o app tiver um backend rápido e fontes de dados e seus servidores estiverem localizados próximos aos usuários (CDN).
O quanto um servidor pode enviar por faze de conexão por seguimento
O papel do servidor em auxiliar e tornar a apresentação de conteúdo mais rápida é certamente específica à aplicação. A solução nem sempre tão óbvia quanto "renderize a página inteira no servidor".
Em alguns casos, deixar fora da resposta inicial partes da página que não sejam essenciais para o que o usuário está provavelmente buscando seja melhor, e então serem buscadas depois pelo cliente. Algumas aplicações, por exemplo, optam por renderizar a "concha" da página para responder imediatamente. Então buscam diferentes porções da página em paralelo. Isto permite que haja grande capacidade de resposta mesmo em situações em que haja um serviço de backend lento. Para algumas páginas, pré-renderizar o conteúdo que está "acima da dobra" também é uma opção viável.
Fazer uma avaliação qualitativa de scripts e estilos baseada na informação que o servidor tem sobre a sessão, o usuário e a URL é absolutamente crucial. Os scripts que lidam com organização de pedidos irão ser obviamente mais importantes para /orders (em inglês, "pedidos") do que a lógica de ligar com a página de configurações. Talvez de maneira menos intuitiva, também pode ser feita uma distinção entre "CSS estrutural" e o "skin/tema CSS". O primeiro pode ser requisitado pelo código JavaScript, então deve ser bloqueado, mas o segundo pode ser carregado de forma assíncrona.
Um bom exemplo de um SPA que não incorre em penalidades de rodadas extras é um clone prova-de-conceito de Stack0verflow em 4096 bytes (que pode em teoria ser entregue na primeira rodada logo após a inicial de uma conexão TCP!). Ele consegue retirar este às custas da capacidade de ser colocado em cache, alinhando todos os ativos dentro da resposta. Com SPDY ou HTTP/2 server push, pode ser teoricamente possível entregar o código de cliente com capacidade de cache em um único salto. Por enquanto, renderizar parte ou toda a página no servidor é a solução mais comum para evitar rodadas extras.
SPA prova-de-conceito com CSS alinhado e JS que não incorre em rodadas extras
Um sistema flexível o suficiente para compartilhar renderização de código entre o servidor e o browser e que forneça ferramentas para carregar progressivamente scripts e estilos irá provavelmente eliminar a distinção coloquial entre websites e webapps. Ambos são comandados pelos mesmos princípios de UX (User Experience). Um blog e um CRM (do Inglês, "Custom relationship management" ou Gestão de Relacionamento com o cliente) não são, fundamentalmente, tão diferentes. Eles possuem URLs, navegação, mostram informações ao usuário. Mesmo uma aplicação de planilha, que geralmente depende muito mais de funcionalidades do cliente, primeiro precisa mostrar ao usuário os dados que ele estiver interessado em modificar. E fazer isso na menor quantidade de rodadas é essencial.
No meu ponto de vista, os maiores negócios em performance vistos em muitos sistemas extremamente empregados atualmente tem a ver com o acúmulo progressivo de complexidade. Tecnologias como o JavaScript e CSS foram adicionadas com o tempo. Suas popularidades aumentaram com o tempo também. Apenas agora podemos apreciar o impacto das diferentes formas que foram aplicados. Algumas destas questões são resolvidas melhorando protocolos (como mostrado pelas melhorias que estão sendo vistas no SPDY E QUIC), mas a camada da aplicação é de onde a maior parte dos benefícios virão.
É útil se referir a algumas das discussões iniciais acerca do design inicial do WWW e HTML para entender isso. Em particular, esse tópico de uma lista de discussão de 1997 propondo a adução da tag <img> ao HTML. Marc Anderson reitera a importância de servir informação rapidamente:
Se um documento precisar ser reunido na hora, pode se tornar arbitrariamente complexo, e mesmo se isso for limitado, nós certamente teríamos um grande sucesso com performance para documentos estruturados desta forma. Isto essencialmente cria o princípio do salto único da WWW (bem, IMG também cria, mas por uma razão bem específica e de uma forma bem limitada) - temos certeza que queremos fazer isso?
2. Aja imediatamente no input de usuário
JavaScript nos permite mascarar a latência da rede por completo. Aplicando isso como princípio de design deve até remover a maior parte das mensagens de "carregando" de suas aplicações. PJAX ou TurboLinks perdem oportunidades de melhorar a percepção de velocidade.
O primeiro princípio constrói fortemente a ideia de minimizar a latência quando o usuário interage com seu website.
Isto tendo sido dito, não importa quanto esforço você invista em minimizar o vai-e-vem entre o servidor e o cliente, existem algumas coisas que estão além do seu controle. Um limite inferior teórico dado pela distância entre seu usuário e seu servidor sendo uma questão inescapável.
Qualidade ruim ou imprevisível de rede sendo outra. Se a conexão de rede não é boa, re-transmissão do pacote irá ocorrer. O que você esperaria que levasse algumas rodadas pode acabar levando várias.
E nisso reside a maior força do JavaScript em melhorar a experiência de usuário. Com código client-side comandando a interação do usuário, nós agora podemos mascarar a latência. Podemos criar a percepção de velocidade. Podemos artificialmente nos aproximar de latência zero.
Vamos considerar HTML básico novamente por um segundo. Documentos conectados juntos através de hiperlinks ou tags <a>. Quando quaisquer um deles são clicados, o browser fará um pedido de rede que levará um tempo imprevisivelmente longo, então receber e processar a resposta e finalmente fazer a transição para o novo estado.
O JavaScript permite ação imediata e optimizada no input de usuário. Um clique em um link ou botão irá resultar em uma reação imediata sem atingir a rede. Um exemplo famoso disto é o Gmail (ou Google Inbox), onde arquivar um email acontecerá imediatamente na UI (do Inglês, "User Interface") enquanto o pedido do servidor é enviado e processado de forma assíncrona.
No caso de um formulário, ao invés de aguardar por HTML como resposta depois de uma submissão, podemos agir logo após o usuário pressionar enter. Ou ainda melhor, como a pesquisa do Google faz, podemos responder ao usuário assim que ele pressiona uma tecla:
O Google adapta seu layout no momento em que você pressiona uma tecla
Este comportamento particular é um exemplo do que eu chamo de adaptação de layout. O princípio básico é que o primeiro estado da página "sabe" sobre o layout do próximo estado, então ele pode fazer a transição para o outro antes que haja dados para popular a página. É "otimista" porque ainda há um risco de que os dados nunca venham e que um erro aconteça ao invés disso, mas isso é obviamente raro.
A página inicial do Google é particularmente relevante para este artigo porque sua evolução ilustra os primeiros dois princípios que discutimos de forma muito clara.
Primeiro de tudo, analisando o envio de pacote da conexão TCP para o www.google.com revela que a página inicial inteira é enviada de uma única vez depois que o pedido chega. A troca completa, incluindo fechar a conexão, leva 64ms para mim em São Francisco. Isso provavelmente tem sido o caso desde o princípio.
No final de 2004, o Google foi pioneiro no uso de JavaScript para criar o recurso de sugestões em tempo real (curiosamente, como um projeto de 20% do tempo, assim como o Gmail). Isso inclusive se tornou uma inspiração para cunhar o AJAX:
Dê uma olhada no Google Suggest. Veja a forma com que os temos sugeridos se adaptam à medida em que você digita, quase que instantaneamente, sem ter que se esperar pelas páginas carregarem. Google Suggest e Google Maps são dois exemplos de uma nova abordagem para aplicações web que nós do Adaptative Path chamamos Ajax.
E em 2010 eles introduziram o Instant Search, que coloca o JS na frente e no centro evitando que a página seja recarregada completamente e fazendo transição para o layout de "resultados de pesquisa" assim que você pressiona alguma tecla, como vimos acima.
Outro exemplo proeminente de adaptação de layout está mais provavelmente no seu bolso. Desde o princípio, o iPhone OS pede aos autores de apps que forneçam uma imagem default.png para ser renderizada imediatamente, enquanto o aplicativo está sendo carregado.
iPhoneOS carrega a imagem default.png antes do aplicativo
Neste caso, o OS (do inglês, "Operational System", ou sistema operacional) estava compensando não necessariamente pela latência de rede, mas pela CPU. Isto foi crucial considerando as restrições do hardware original. Existe, porém, um cenário onde esta técnica falha. Isto seria quando o layout não combina com a imagem armazenada, como no caso das telas de login. Uma análise detalhada de suas implicações foi feita por Marco Arment em 2010.
Outra forma de input além de cliques e submissão de formulários que é altamente melhorada pelo JavaScript é o input de arquivos
Podemos capturar a intenção do usuário de fazer upload através de uma variedade de meios: arrastar e soltar, colar, seletor de arquivos. Então, graças às new APIs de HTML5, podemos mostrar conteúdo como se tivesse sido feito upload dele. Um exemplo desta ação está no novo trabalho que fizemos com Cloudup uploads. Perceba como um thumbnail é gerado e renderizado automaticamente:
A imagem é redenrizada e apagada antes do carregamento
Em todos estes casos, estamos melhorando a percepção de velocidade. Felizmente, existem muitas evidências de que esta é uma boa idéia. Considere este exemplo de como aumentar a esteira de bagagem diminuiu o número de reclamações no aeroporto de Houston, sem necessariamente fazer com que a bagagem fosse entregue mais rápido.
A aplicação desta idéia deve ter várias implicações profundas na UI (interface de usuário) de suas aplicações. Eu mantenho a afirmação de que indicadores de que a página está carregando devem se tornar raridade, especialmente considerando que estamos em transição para aplicações com dados ao vivo, discutido na próxima parte.
Existem situações em que a ilusão de imediatismo pode realmente ser prejudicial para a experiência do usuário. Considere um formulário de pagamento ou um link de logout. Agindo de forma otimista nestes, dizendo ao usuário que tudo está feito quando na verdade não está pode resultar em uma experiência negativa.
Mas mesmo nestes casos, mostrar que a página está carregando devem ser deferidos. Eles devem apenas ser renderizados quando o usuário não considere mais que a resposta foi imediata. De acordo com a tão falada pesquisa de Nielsen:
O conselho básico acerca de tempos de resposta foi aproximadamente o mesmo por trinta anos Miller 1968; Card et al. 1991:
0.1 é aproximadamente o limite para que o usuário sinta que o sistema está reagindo instantaneamente, significando que nenhum feedback especial é necessário exceto mostrar o resultado.
1.0 segundo é aproximadamente o limite para que o fluxo de pensamento do usuário fique ininterrupto, mesmo que o usuário não perceba o delay. Normalmente, nenhum feedback especial é necessário durante delays de mais de 0.1 mas de menos de 1.0 segundo, mas o usuário perde o sentimento de estar operando diretamente os dados.
10 segundos é aproximadamente o limite para manter a atenção do usuário focada no diálogo. Para delays mais longos do que isso, o usuário irá fazer outras tarefas enquanto espera que o computador aja.
Técnicas como PJAX ou TurboLinks infelizmente perdem grandes oportunidades descritas nesta sessão. O código client-side não "sabe" sobre a futura representação da página até que a rodada inteira para o servidor ocorra.
Confira a próxima parte do artigo aqui: 7 Princípios de Aplicações Rich Web - parte II