Android

20 jul, 2017

Construindo m.uber: engenhando um aplicativo web de alto desempenho para o mercado global

Publicidade

À medida que a Uber se expande para novos mercados, queremos permitir que todos os usuários solicitem rapidamente uma viagem, independentemente da localização, da velocidade da rede e do dispositivo. Com isso em mente, reconstruímos nosso web client do zero como uma alternativa viável para o app mobile nativo.

Compatível com todos os navegadores modernos, o m.uber oferece uma experiência semelhante a um aplicativo para passageiros em dispositivos low-end, incluindo aqueles que não são suportados pelo nosso cliente nativo. O aplicativo também é pequeno – o principal aplicativo de solicitação de viagem vem com apenas 50kb, permitindo que o aplicativo seja carregado rapidamente mesmo em redes 2G.

Neste artigo, descrevemos como construímos m.uber (pronunciado moo-ber) e exploramos o desafio de implementar a experiência do aplicativo nativo em um aplicativo web super-leve.

m.uber imita o fluxo do aplicativo Uber nativo, permitindo que os passageiros especifiquem seu pedido de viagem e acompanhem a localização do motorista após serem correspondidos.

 

Menor, mais rápido: como nós o construímos

m.uber está escrito em ES2015+, usando Babel para transpilação ES5. O desafio de projeto abrangente foi minimizar o rastro do cliente, mantendo a rica experiência do aplicativo nativo. Assim, enquanto nossa arquitetura tradicional utiliza React (com Redux) e Browserify para o agrupamento de módulos, trocamos no Preact por seus benefícios de tamanho e Webpack por suas capacidades de divisão de pacotes dinâmicos e tree shaking. Abaixo, discutimos como abordamos esses e outros desafios em toda a arquitetura do aplicativo:

 

Renderização do servidor inicial

Os clientes não podem começar a renderizar a marcação até que todos os pacotes principais de JavaScript tenham sido baixados, então m.uber responde à solicitação inicial do navegador ao renderizar o Preact no servidor. O estado e a marcação resultantes são incorporados como strings na resposta do servidor para que o conteúdo seja carregado quase que imediatamente.

 

Sirva pacotes sob demanda

O objetivo do m.uber é permitir que os usuários solicitem uma viagem o mais rápido possível, mas muito do nosso JavaScript é para tarefas auxiliares: atualização de opções de pagamento, verificação do progresso de uma viagem ou configurações de edição. Para garantir que só estamos fornecendo o JavaScript que precisamos, usamos o Webpack para a divisão de código.

Usamos uma função splitPage que retorna o pacote auxiliar envolvido em um componente assíncrono. Por exemplo, a página de configurações é chamada pela função abaixo:

const AsyncSettings = splitPage(
 {load: () => import(‘../../screens/settings’)}
);

Usando esta função, o pacote de configurações será buscado somente se e quando AsyncSettings for incluído condicionalmente pelo método de renderização principal. Para conexões muito lentas, AsyncSettings renderizará um “modo de carregamento” pendente da conclusão da busca do pacote.

 

Pequenas bibliotecas

O m.uber foi projetado para ser rápido mesmo em redes 2G, então o tamanho do client é crítico. Nosso aplicativo principal (a parte essencial do aplicativo que permite que você solicite uma viagem) vem com apenas 50kB gzipado e minificado, o que significa uma terceira vez para a interação em redes típicas de 2G (250kB/s, latência 300ms). Abaixo, destacamos a diferença no tamanho e número de dependências do fornecedor agora e no início do projeto m.uber:

O tamanho do pacote do fornecedor m.uber e o número de dependências são muito menores do que quando o projeto começou.

 

Preact sobre React

No interesse do tamanho, escolhemos o Preact (3kB GZip/minificado) sobre React (45kB). O Preact pode fazer quase tudo o que React faz (não suporta PropTypes ou eventos sintéticos) e adiciona alguns recursos de reflexão agradáveis. O Preact é um pouco excessivo quando se trata de componentes e elementos de reciclagem (mas eles estão trabalhando nisso), o que significa que você pode precisar definir chaves em elementos que não esperaria, mas, de outra forma, funcionou muito bem para nossas necessidades.

 

Dependências mínimas

Para combater o inchaço da dependência, fomos seletivos sobre os pacotes npm utilizados no client, fazendo uso de bibliotecas como Just, cujos módulos são responsáveis apenas por uma função e não têm dependências. Descobrimos que fazia sentido restringir as caras transformações de dados ao servidor, de modo que os módulos mais pesados como Moment não precisassem ser baixados. Para identificar fontes de inchaço de dependência, fizemos uso intenso de ferramentas como source-map-explorer.

 

Conjunto de Recursos Condicionais

A missão do m.uber é dar a todos, em todos os lugares, a capacidade de solicitar facilmente uma viagem e fornecer recursos adicionais quando o dispositivo e a rede os permitem. Detectamos o tempo para a primeira interação usando o window.performanceAPI e escondemos ou carregamos a experiência do mapa interativo com base no resultado. O mapa também pode ser ativado e desativado na página de configurações para usuários cujo desempenho de rede não possamos detectar.

 

Chamadas render Mínimas

O Preact (como o React) usa um VDOM para gerar nova marcação quando ocorre uma alteração, mas isso não significa que chamar render seja gratuito. É preciso muita conversa de JavaScript para render descobrir que nada precisa acontecer. Usamos shouldComponentUpdate extensivamente para minimizar chamadas para render.

 

Armazenamento em Cache

Service Workers

Os servisse workers interceptam solicitações de URL, permitindo que as buscas de rede e de disco locais sejam substituídas pela lógica de busca personalizada, que geralmente alavanca a API Cache do navegador. Ao armazenar em cache a resposta HTML inicial, bem como os pacotes de JavaScript, os servisse workers permitem que o m.uber continue a servir conteúdo em caso de perda de rede intermitente.

Os servisse workers também podem diminuir significativamente os tempos de carregamento. O desempenho de I/O de disco varia muito em sistemas operacionais e dispositivos e, em muitos casos, mesmo obter dados do cache de disco é frustrantemente lento. Onde os servisse workers são suportados, todo o conteúdo reencaminhado (incluindo HTML) vem diretamente do cache do navegador, permitindo que as páginas sejam recarregadas imediatamente.

Os clients m.uber instalam um novo servisse worker após cada compilação. Como o WebPack gera nomes de pacotes dinâmicos, nosso processo de compilação grava novos nomes diretamente no módulo do servisse worker. Na instalação, nós armazenamos em cache nossas principais bibliotecas de JavaScript, então preguiçosamente armazenamos em cache HTML e pacotes de JavaScript auxiliares à medida que são buscados.

 

Armazenamento local

Onde precisamos armazenar em cache dados de resposta que são muito voláteis para os servisse workers, nós os salvamos no armazenamento local do navegador. m.uber pesquisa para o status da viagem a cada poucos segundos; manter os dados de status mais recentes no armazenamento local significa que quando um passageiro retorna ao aplicativo, podemos re-renderizar rapidamente a sua página sem esperar uma viagem de ida e volta para a API. Uma vez que nossos dados de status são pequenos e o tamanho de dados armazenados é finito, as atualizações de armazenamento são rápidas e confiáveis ​​e, finalmente, descobrimos que não precisávamos usar uma API de armazenamento local assíncrona como indexedDB.

 

Estilo

Styletron

Os estilos são definidos como objetos JavaScript em cada componente. Quando um componente é renderizado, o Styletron gera dinamicamente stylesheets a partir dessas definições. Colocação de estilos com componentes permite uma fácil divisão de pacote e carregamento assíncrono de estilos. O CSS que não é usado nunca é carregado.

Styletron de-duplica as declarações de estilo criando uma stylesheets atômica para cada regra exclusiva, permitindo um tempo de execução CSS mínimo e o melhor desempenho de renderização da classe. Usamos o Styletron para toda geração de CSS de nível de componente no m.uber.

 

SVGs

Para economizar espaço, usamos o formato SVG para imagens semelhantes a ícones sempre que possível, e as colocamos na linha no método render. Para o ajuste, utilizamos o SVGO juntamente com otimizações manuais para reduzir ainda mais os caminhos. Às vezes, conseguimos substituir polilinhas com formas básicas e usamos as dimensões da caixa de visualização com divisores adequados para evitar decimais caros nos caminhos.

O impacto desta estratégia no tamanho geral do aplicativo é significativo; por exemplo, reduzimos o tamanho do nosso logotipo de 7.4kB (png) para 500 bytes (SVG ajustado).

 

Fontes

Com o uso judicioso de tamanho e cor, descobrimos que conseguimos eliminar completamente as fontes personalizadas, sem comprometer significativamente o projeto visual.

 

Manipulação de erros

Uma pilha de tecnologia simples nem sempre é propícia ao diagnóstico fácil de erros, então adicionamos algumas ferramentas leves para ajudar, por exemplo:

  • Em vez de usar uma biblioteca de monitoramento de erro pesada, prolongamos onerror para enviar erros a um reportador de erro de client no servidor.
  • Nós curto-circuitamos os erros recursivos do método do ciclo de vida envolvendo o render do Preact e shouldComponentUpdate.
  • Em nosso projeto, os erros lançados por um arquivo hospedado por CDN não fornecerão dados úteis para onerror, a menos que as permissões apropriadas sejam fornecidas por cabeçalhos de compartilhamento de recursos de origem cruzada (CORS). Mesmo com esses cabeçalhos, no entanto, os erros lançados durante um evento assíncrono não podem ser rastreados de volta para o módulo principal e, portanto, window.onerror permanecerá no escuro. Envolvemos todos os ouvintes de eventos para permitir que os erros fossem passados para o módulo principal via tentativa/captura.

 

Próximos passos

Através do nosso trabalho com o m.uber, nós colocamos muito esforço na criação de uma experiência nativa e similar a um aplicativo em um pacote de desempenho, mas não terminamos – ainda há muitas oportunidades de melhoria. Nos próximos meses, estamos planejando lançar otimizações adicionais, incluindo:

  • Formalizar uma estratégia para minimizar as chamadas de renderização, pois os componentes apenas aceitam uma coleção plana de suportes primitivos e de série. Isso nos permitirá usar o pureComponent (que implementa automaticamente shouldComponentUpdate com uma comparação de suporte superficial) e render para focar a geração de marcação em vez de lógica de ramificação e outras tarefas tangenciais. Transformar as respostas da API às primitivas achatadas pode ser delegada à lógica do servidor (ver normalizr) e/ou mapStateToProps, conforme apropriado.
  • Combinar ações e reductors, o que tornaria a separação de pacotes mais intuitiva.
  • Usar HTTP/2 para todos os pedidos e substituir as APIs de pesquisa com notificações Push.

 

Além disso, estamos abstraindo as peças de infraestrutura do m.uber em uma arquitetura de código aberto que servirá de base para futuros leves aplicativos da Uber na internet – fique atento para um próximo artigo sobre esse tópico.

***

Este artigo é do Uber Engineering. Ele foi escrito por Angus Croll. A tradução foi feita pela Redação iMasters com autorização. Você pode conferir o original em: https://eng.uber.com/m-uber/