7 Princípios de Aplicações Rich Web
Este é um artigo baseado em uma apresentação que fiz na BrazilJS em Agosto de 2014. Se baseia em algumas das idéias que eu tenho escrito no meu blog recentemente a respeito de UX e performance.
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 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
tl;dr: 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 é renderizada e então desaparece antes do upload ser completo
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.
3. Reaja à mudanças de dados
tl;dr: Quando os dados mudam no servidor, deixe os clientes saberem sem perguntar. Esta é uma forma de melhoria de performance que libera o usuário de ações manuais para atualizar (F5, puxar a tela para baixo). Novos desafios: controle de (re)conexão, reconciliação de estado.
O terceiro princípio é que a capacidade de resposta da UI (do inglês, "User Interface") em relação a mudanças de dados na fonte, tipicamente um ou mais servidores de database.
Servir um HTML instantâneo do dado que permanece estático até que o usuário atualize a página (websites tradicionais) ou interaja com ele (AJAX) está cada vez mais se tornando obsoleto.
Sua interface deve se auto-atualizar.
Isto é crucialmente importante num mundo com um número cada vez maior de pontos de informação, na forma de relógios, celulares, tablets e dispositivos que se possa usar no corpo que ainda serão inventados.
Considere o feed de notícias do Facebook no momento de sua criação, quando informações eram compartilhadas primariamente através de computadores pessoais. Renderizar de forma estática não era o ideal, mas fazia sentido se as pessoas estivessem atualizando seus perfis talvez uma vez por dia, se tanto.
Agora vivemos em um mundo em que você pode fazer upload de uma foto e seus amigos podem curti-la ou comentá-la quase que imediatamente. A necessidade de feedback em tempo real é natural devido ao uso muito maior da aplicação.
Seria errado, porém, assumir que os benefícios da capacidade de resposta são limitados a aplicações multi-usuário. Que é o motivo pelo qual eu gosto de falar sobre pontos de dados concorrentes como opostos a usuários. Considere o cenário comum de compartilhar uma foto que você tem no seu celular com o seu próprio laptop.
Uma aplicação single-user ainda pode se beneficiar da capacidade de reação
É útil pensar em toda a informação exposta ao usuário como reativa. Sincronização de estado de sessão e login são um exemplo de aplicação deste princípio de maneira uniforme. Se os usuários da sua aplicação tiverem múltiplas abas abertas simultaneamente, deslogar de uma delas invalidará todas as outras. Isto inevitavelmente resulta em melhorias de privacidade e segurança, especialmente em situações onde várias pessoas têm acesso ao mesmo dispositivo.
Cada página reage à sessão e ao estado de login
Uma vez que você crie a expectativa de que a informação na tela se atualizará automaticamente, é importante considerar uma nova necessidade: reconciliação de estado.
Quando recebemos atualizações de dados atômicos ordenados, é fácil de esquecer que sua aplicação deve ser capaz de atualizar apropriadamente mesmo depois de longos períodos desconectada. Considere o cenário de fechar a tampa do seu laptop e reabri-la dias depois. Como seu app se comporta então?
Exemplo do que ocorreria caso desconsiderássemos o tempo decorrido até a reconexão
A capacidade da sua aplicação reconciliar estados deslocados no tempo também é relevante para o nosso primeiro princípio. Se você optar por enviar dados com o carregamento inicial da página, você deve considerar o tempo que o dado levará até que os seus scripts client-side carreguem. Este tempo é essencialmente equivalente a uma desconexão, e a conexão inicial dos seus scripts é a retomada da sessão.
4. Controle a troca de dados com o servidor
tl;dr: Agora nós podemos afinar a troca de dados com o servidor. Certifique-se de lidar com os erros, fazer uma nova tentativa pelo usuário, sincronizar dados em plano de fundo e manter caches offline.
Quando a WWW foi criada, troca de dados entre cliente e servidor era limitada a algumas poucas maneiras:
1. Clicar em um link resultaria (GET) uma nova página que seria renderizada.
2. Enviar um formulário iria postar (POST) ou obter (GET) e renderizar uma nova página.
3. Anexar uma imagem ou objeto iria enviá-lo (GET) de forma assíncrona e então renderizá-lo.
A simplicidade deste modelo é atrativa, e nós certamente temos uma curva de aprendizado maior hoje quando o assunto é entender como dados são enviados e recebidos.
As maiores limitações estavam relacionadas ao segundo ponto. A inabilidade de enviar dados sem necessariamente carregar uma nova página não era o ideal do ponto de vista de performance. Mas, mais importante, inviabilizava completamente o botão de voltar:
Possivelmente o artefato mais irritante da antiga web
A web como uma plataforma de aplicações foi, assim, inconcebível sem JavaScript. O AJAX constituiu um salto em termos de experiência de usuário acerca do envio de informação.
Agora temos uma variedade de APIs (XMLHttpRequest, WebSocket, EventSource para dar alguns exemplos) que nos dão um controle mais fino do fluxo de dados. Em adição à possibilidade de enviar dados que o usuário insira em um formulário, agora temos algumas novas oportunidades na melhoria de UX (User Experience).
Uma que é especialmente relevante ao princípio anterior é a habilidade de mostrar o estado de conexão. Se criarmos a expectativa de que a informação se atualiza automaticamente, devemos notificar o usuário sobre ser desconectado e sobre tentativas de reconexão.
Quando uma desconexão é detectada, é útil que se armazene dados na memória (ou até melhor, localStorage) para que possa ser enviada depois. Isto é especialmente importante em luz da introdução doc ServieWorker, que possibilita que aplicações web JavaScript rodem em background. Se sua aplicação não estiver aberta, você ainda pode tentar sincronizar dados do usuário em background.
Considere timeouts e erros quando se envia dados e se faz uma nova tentativa em nome do usuário. Se uma conexão é restabelecida, se tenta enviar os dados novamente. Em caso de persistência do problema, entre em contato com o usuário.
É necessário lidar com certos erros de forma cautelosa. Por exemplo, um erro 403 inesperado pode significar que a sessão do usuário foi invalidada. Nestes casos, você tem a oportunidade de levar o usuário a retomá-la mostrando uma tela de login.
Também é importante ter certeza que o usuário não interrompa o fluxo de dados inadvertidamente. Isto pode acontecer em duas situações. A primeira e mais óbvia é fechando o browser ou aba, que você pode tentar evitar com o "beforeunload"
O aviso de beforeunload no browser
O outro (e menos óbvio) é capturar transições de página antes que elas aconteçam, como clicar em links que façam com que uma nova página seja carregada. Isto te dá uma chance de mostrar seus próprios avisos.
5. Não mude a história, faça melhorias
tl;dr: Sem o browser gerenciando URLs e histórico para nós, novos desafios são criados. Garanta que expectativas ligadas a rolar a página não serão quebradas. Mantenha seus caches para resposta rápida.
Envios de formulário de lado, se tivéssemos que desenvolver qualquer aplicação web moderna apenas com hiperlinks, acabaríamos com uma navegação completamente funcional no sentido de voltar e avançar.
Considere, por exemplo, o típico "cenário de paginação infinita". A forma típica que é implementado envolve capturar o clique com JavaScript, fazendo o pedido de dados / HTML, injetando. Fazendo com que a chamada history.pushState ou replaceState chamem seu passo opcional, que infelizmente não é dado pela maioria.
E esta é a razão pela qual sempre uso a palavra "quebra". Com o modelo mais simples que a web propôs inicialmente, esta situação não estava em jogo. Todo estado de transição necessitava de uma mudança de URL.
O outro lado disso é que surgem novas oportunidades para melhorar a história agora que podemos controlá-la com JavaScript.
Uma oportunidade é o que Daniel Pipius chamou de Fast Back:
Voltar deve ser rápido; usuários não esperam que os dados tenham mudado muito.
Isto é semelhante ao considerar o botão de voltar um botão em nível de aplicação e aplicar o princípio 2: aja imediatamente no input de usuário. A chave é que você agora pode decidir como fazer cache da página anterior e renderizar instantaneamente. Você pode então aplicar o princípio 3 e então informar o usuário das novas mudanças de dados que ocorreram à página.
Ainda existem alguns casos onde você não estará em controle do comportamento do cache. Por exemplo, se você renderizar uma página, e então navegar para um website de terceiros, e então o usuário pressiona o botão de voltar. Aplicações que renderizem o HTML no servidor e então o modifiquem no cliente estão em risco particular deste bug súbito:
Pressionar o botão de voltar incorretamente carrega o HTML inicial da página
Outra forma de quebrar a navegação é ignorando a memória de rolagem. Mais uma vez, páginas que não se apoiem em JS e gerenciamento de histórico manual muito provavelmente não tenham problemas em relação a isto. Mas as dinâmicas normalmente têm. Eu testei os dois mais populares newsfeeds JavaScript da web: Twitter e Facebook. Ambos exibiram amnésia de rolagem.
Paginação infinita é geralmente suscetível a amnésia da barra de rolagem
Finalmente, esteja ciente de que mudanças de estado são relevantes apenas quando se está navegando pelo histórico. Considere este exemplo de quando contraímos as sub-árvores de comentários.
A contração dos comentários deveria ser preservada quando navegamos pelo histórico
Se a página fosse re-renderizada ao se seguir um link dentro da aplicação, a expectaria do usuário poderia ser de que todos os comentários aparecessem da forma que estavam. O estado era volátil e apenas associado com o histórico.
6. Atualizações de push code
tl;dr: Publicar dados sem um pushing code é insuficiente. Se seus dados se atualizam automaticamente, seu código também deve o fazer. Evite erros de API e melhore a performance. Use DOM estateles para evitar os efeitos colaterais.
Fazer sua aplicação reagir a mudanças de código é crucialmente importante.
Primeiro de tudo, isso reduz a possibilidade de erros e melhora a segurança. Se você fizer uma mudança nos seus APIs de backend, então o código do cliente deve ser atualizado. De outra maneira, eles podem não ser capazes de entender novos dados, ou podem enviar os dados em formato incompatível.
Outra razão igualmente importante tem a ver com a implementação do princípio #3. Se a sua UI (do inglês, "User Interface") se atualiza sozinha, existem poucos motivos para que os usuários atualizem a página.
Mantenha em mente que em um website tradicional, atualizar uma página tem duas partes: recarregar os dados e recarregar o código. Criar um mecanismo para carregar dados sem carregar o código não é suficiente, especialmente em um mundo em que uma única aba (sessão) pode ficar aberta por um longo período de tempo.
Se o canal de push de um servidor estiver no local, uma notificação pode ser emitida para clientes quando um novo código estiver disponível. Na falta disso, um número de versão pode ser anexado ao cabeçalho para pedidos de HTTP de saída. O servidor pode então compará-lo à sua última versão conhecida, optar por lidar com o pedido ou não e aconselhar o cliente.
Depois disso, algumas aplicações web optam por atualizar a página pelo usuário quando for apropriado. Por exemplo, se a página não estiver visível e nenhum formulário for preenchido.
Uma abordagem melhor seria fazer um carregamento de hot code. Isso significa que não haveria necessidade de recarregar a página completamente. Ao invés disso, alguns módulos podem ser trocados em tempo real e seus códigos re-executados.
É certamente difícil fazer fazer recarregamento de hot code funcionar para muitos codebases existentes. Vale a pena discutir então um tipo de arquitetura que possa separar de maneira elegante o comportamento (código) dos dados (estado). Tal separação nos permitiria criar uma gama de patches de forma eficiente.
Considere por exemplo um módulo da sua aplicação que cria um event bus (e.g.: socket.io). Quando os eventos são recebidos, o estado de um certo componente é populado e então é feita a renderização para o DOM. Então você modifica o comportamento daquele componente, por exemplo, para que produza diferentes marcações de DOM para novos e existentes estados.
Mas o próximo desafio é que módulos devem ser capazes de serem capazes de serem re-avaliados sem introduzirem efeitos colaterais indesejáveis. Isso é quando uma arquitetura como a proposta pelo React se torna particularmente útil. Se um componente de código é atualizado, sua lógica pode ser re-executada de forma trivial e o DOM atualiza de forma eficiente. Uma exploração deste conceito por Dan Abramov pode ser encontrada aqui.
Em essência, a idéia de que você renderiza para o DOM é o que ajuda de forma significante com a mudança de código. Se o estado fosse mantido no DOM, ou ouvintes do evento fossem criados manualmente pela sua aplicação, atualizar o código se tornaria uma tarefa muito mais complicada.
7. Preveja o comportamento
tl;dr: Latência negativa
Uma aplicação JavaScript rica pode ter mecanismos que consigam predizer o eventual input de usuário.
A aplicação mais comum desta idéia é a de preemptivamente requerer dados do servidor antes de uma ação ser consumada. Começando a buscar dados quando você passar o cursor por cima de um hiperlink para que esteja pronto quando for clivado é um exemplo disto.
Um método um pouco mais avançado é monitorar movimentos do mouse e analisar sua trajetória para detectar "colisões" com elementos acionáveis como botões. Um exemplo jQuery:
Plugin jQuery que prediz a trajetória do mouse
Conclusão
A web continua sendo um dos meios mais versáteis para transmissão de informação. Enquanto continuamos adicionando mais dinamismo para nossas páginas, devemos garantir que vamos manter alguns de seus grandes benefícios históricos, enquanto nós incorporamos novos.
Páginas interconectadas por hiperlinks são um grande bloco de construção para qualquer tipo de aplicação. Carregamento progressivo de código, estilo e marcação enquanto o usuário navega através delas irá garantir ótima performance sem sacrificar a interatividade.
Novas oportunidades únicas foram possibilitadas pelo JavaScript que, uma vez que tenha sido universalmente adotado, irá garantir a melhor experiência de usuário possível para a maior e mais livre plataforma existente.