Quando se pensa sobre o desempenho de navegadores, a guerra de VMs JavaScript é a primeira coisa que vem à mente. O que é totalmente justificável, uma vez que estamos fazendo aplicações cada vez mais ricas e ambiciosas no lado do cliente do navegador. De fato, de acordo com o HTTP Archive, a quantidade de código JavaScript em uma página média quase dobrou só no último ano (chegando a 194kB).
Entretanto, no mesmo período, o tamanho médio de uma página cresceu para 1059kB (mais de 1MB!) e agora ela é composta de mais de 80 sub-requisições de elementos – pense um minuto sobre isso. Carregar todo esse conteúdo pode ser tudo menos simples e, assim como parece, a pilha de rede de um navegador como o Chrome é, por si só, um componente cada vez mais importante e que vale a pena compreender quando se trata de otimizar o desempenho da navegação.
Chrome e o carregamento de recursos multiprocessos
Cada aba do navegador no Chrome tem seu próprio processo isolado, o que nos propicia uma grande separação e muitos benefícios de segurança. Entretanto, toda a comunicação de rede é manipulada pelo processo principal do navegador. Sempre que uma aba precisa carregar um recurso remoto, ele envia uma requisição IPC para o processo no host e espera pela resposta.
Isso pode parecer contraproducente à primeira vista, mas há muitas razões para uma arquitetura como essa: o navegador é capaz de controlar toda a atividade de rede de cada aba (segurança), ele pode limitar o número de conexões por host, assim como pode fornecer um pool de recursos e reutilizá-los, e isso também permite que mantenhamos um cache HTTP consistente e o estados das sessões (cookies e outros dados em cache).
Administrar todas essas interações é, para início de conversa, uma tarefa nada trivial, mas o que é mais interessante é o nível de otimização que há nessa camada para evitar a latência de rede: dada uma URL remota, precisamos resolver o DNS, realizar o handshake TCP e só depois poderemos enviar a requisição. Uma busca DNS leva em média de 60-120ms, seguido de todo o trajeto de ida e volta (full round-trip ou RTT) para realizar o handshake TCP – tudo isso cria uma latência de 100-200ms antes que possamos sequer enviar uma requisição! Então, o que poderia o navegador fazer para nos ajudar a compensar esse custo?
Pré-carregamento de DNS e TCP
No núcleo da pilha de redes do Chrome, há um único objeto Predictor, que sozinho é o único responsável por antecipar o comportamento do usuário, assim como requisitar os recursos que cada aba pode precisar em um futuro próximo. Pondo em outras palavras, o Chrome aprende sobre a topologia da rede na medida em que você o utiliza. Se ele fizer o trabalho direito, então poderá resolver antecipada e especulativamente os hostnames (resolução de DNS), assim como abrir as conexões (pré-conexão TCP) antes que ela seja necessária.
Para isso, o Predictor necessita otimizar um grande número de restrições: pré-carregamento e pré-conexão especulativa não devem impactar o desempenho do carregamento atual, se for muito agressivo pode carregar recursos desnecessários e podemos também nos resguardar contra sobrecarga da rede atual. Para gerenciar esse processo, o predictor conta com dados históricos do navegador, heurística e muitas outras dicas dadas pelo navegador para antecipar as requisições.
Construindo o Predictor do Chrome
A “experiência de inicialização do navegador” possui seu próprio cache separado, no qual o Chrome aprende sobre as 10 primeiras URLs visitadas entre todas as sessões. Sempre que você iniciar o navegador, ele imediatamente resolve todos esses hostnames – uma bela otimização para acelerar a sua rotina matinal! A próxima etapa é o que acontece quando você foca na omnibar e começa a digitar. Se o input se parecer com um termo de busca, então podemos nos pré-conectar com o mecanismo de pesquisa padrão em antecipação à pesquisa. Alternativamente, se o input ou a sugestão for muito provavelmente uma URL, então podemos nos pré-conectar diretamente com host. Se o palpite foi correto, o DNS e o handshake TCP podem terminar antes mesmo de apertarmos o enter!
Em seguida nós requisitamos a URL e o parser começa a criar os tokens a partir dos bytes recebidos para, pouco a pouco, construir a árvore DOM. Dado o modelo de concorrência determinística no navegador, muitos sub-recursos são do tipo blocking – o parser deve parar e esperar pela chegada do recurso. Para ajudar a eliminar o tempo de espera que a rede impõem através desse modelo, o WebKit utiliza um PreloadScanner especulativo, que “olha adiante” no documento e enfileira os recursos remotos. Isso permite que se resolva e carregue alguns recursos antes mesmo que o parser os veja.
Mas mesmo isso não é o ideal. O melhor seria ser capaz de aprender e antecipar a conexão com os subrecursos! De fato, isso é exatamente o que o Chrome faz: ele aprende os domínios dos recursos para cada hostname visitado, e na repetição dos acessos pode resolver e se pré-conectar preemptivamente antes mesmo que o parser veja os primeiros bytes do documento. A imagem abaixo exibe os recursos dos domínios inferidos, assim como as estatísticas para o endereço igvita.com.
Finalmente, ao explorar a renderização da página, ações como passar o mouse sobre um link também podem disparar um pré-carregamento. Cada um desses símbolos redundam numa fila de pré-carregamento FIFO e são rotuladas com um ResolutionMotivation (url_info.h) que permite ao Chrome reordenar e otimizar a ordem de carregamento dos recursos:
enum ResolutionMotivation {
MOUSE_OVER_MOTIVATED, // Mouse sobre o link causa resolução.
PAGE_SCAN_MOTIVATED, // Scan na pág. renderizada causa resolução.
LINKED_MAX_MOTIVATED, // enfileira marcações acima a partir dos links.
OMNIBOX_MOTIVATED, // Omni-box sugere resolver isso.
STARTUP_LIST_MOTIVATED, // Lista inicial causou esta resolução.
NO_PREFETCH_MOTIVATION, // Informação de navegação (não relacionada ao carregamento).
EARLY_LOAD_MOTIVATED, // Em alguns casos usamos o pré-carregamento para preparar a conexão.
// antes de enviar uma requisição real.
UNIT_TEST_MOTIVATED,
// Os seguintes envolvem previsão de pré-carregamento, disparado pela navegação.
// A referrinrg_url_ também é utilizada quando esses são usados.
STATIC_REFERAL_MOTIVATED, // Base de dados externa sugeriu esta resolução.
LEARNED_REFERAL_MOTIVATED, // Navegação anterior nos ensinou esta resolução.
SELF_REFERAL_MOTIVATED, // Palpite sobre a necessidade de uma segunda conexão.
MAX_MOTIVATED // Acima de todas as filas, para uso em histograms.
};
O melhor de tudo é que podemos inspecionar todo esse histórico e caches de execução no navegador (copie os links chrome:// abaixo e cole em uma nova aba):
- chrome://predictors – estatísticas do predictor da omnibox (dica: chuque ‘Filter zero confidences’)
- chrome://net-internals/#sockets – status atual do pool socket
- chrome://net-internals#dns – cache DNS em memória do Chrome
- chrome://histograms/DNS – histogramas do seu desempenho DNS
- chrome://dns – cache de lista de carregamento inicial e de sub-recursos de host
Resolução DNS no Chrome
A resolução de DNS no Chrome merece um tratamento aprofundado, mas é preciso mencionar que, depois de muita deliberação, o time do Chrome está fazendo experiências com seu próprio “solucionador” DNS. Atualmente, o Chrome depende do sistema operacional para realizar a resolução de DNS e para manter um conjunto de 8 threads dedicadas à tarefa. Cada função chamada getaddrinfo() é do tipo blocking, o que também coloca um grande obstáculo à concorrência. Por que oito? Esse é um número empírico baseado no menor denominador comum de hardware – números maiores podem sobrecarregar alguns roteadores domésticos.
Com o novo solucionador assíncrono em funcionamento, o limite pode cair em favor de um solucionador dinâmico, e o Chrome também seria capaz de gerenciar seu próprio cache DNS e realizar mais otimizações, tais como atualização preemptiva de hostnames populares ou em expiração. Para mais detalhes, leia os artigos de Will Chan no Google+: resolução de host no Chromium.
Se estiver curioso, pode habilitar o novo solucionador em chrome://flags (embaixo de “DNS assíncrono integrado”), e você poderá explorar o desempenho da sua atual pilha DNS através dos histogramas embutidos do Chrome. Na sessão abaixo, uma média de buscas DNS levando 84.9ms (ouch):
Latência de rede, mobile e Chrome
Se o predictor do Chrome fizer o trabalho direito, então parte do custo de latência de rede pode ser afastada do usuário. As heurísticas e os algoritmos acima provaram render grandes resultados, mas há ainda muito trabalho a ser feito. Como muitos de nós sabemos de primeira mão, a experiência móvel hoje é com frequência dolorosamente lenta, e isso é em grande parte responsabilidade dos RTTs muito maiores (200-1000ms) em redes sem fio.
De fato, é provável que a melhor otimização que você pode fazer para dispositivos móveis hoje seja reduzir o número de conexões de saída e o número total de bytes da sua página. Latências de rede podem ser tudo, menos livres de custos. O navegador pode com certeza ajudar, como pudemos ver acima, mas cheque o cascateamento da sua rede – seus usuários agradecerão por isso.
***
Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.igvita.com/2012/06/04/chrome-networking-dns-prefetch-and-tcp-preconnect/