Como funciona o JavaScript: dentro da camada de rede + como otimizar seu desempenho e segurança

Robisson Oliveira
React Brasil
13 min readMay 14, 2019

--

Este é o post #12 da série dedicada a explorar JavaScript e seus componentes de construção. No processo de identificação e descrição dos elementos centrais, também compartilhamos algumas regras práticas que usamos ao criar o SessionStack , um aplicativo JavaScript que precisa ser robusto e altamente eficiente para ajudar os usuários a ver e reproduzir seus defeitos do aplicativo da Web em tempo real.

Abaixo estão os posts dessa série publicados até o momento

Assim como mencionamos em nosso post anterior sobre o mecanismo de renderização , acreditamos que a diferença entre bons e grandes desenvolvedores de JavaScript é que os últimos não apenas entendem os detalhes da linguagem, mas também seus internos e o ambiente ao redor.

Um pouco de história

Quarenta e nove anos atrás, uma coisa chamada ARPAnet foi criada. Foi uma rede de comutação antecipada de pacotes e a primeira rede a implementar o pacote TCP/IP . Essa rede estabeleceu um elo entre a Universidade da Califórnia e o Stanford Research Institute. 20 anos depois, Tim Berners-Lee circulou uma proposta de “Malha” que mais tarde ficou conhecida como a World Wide Web. Nesses 49 anos, a internet percorreu um longo caminho, a partir de apenas dois computadores trocando pacotes de dados, chegando a mais de 75 milhões de servidores, 3,8 bilhões de pessoas usando a internet e 1,3 bilhão de sites.

Neste post, tentaremos analisar quais técnicas os navegadores modernos empregam para aumentar automaticamente o desempenho (sem que você saiba) e especificamente ampliaremos a camada de rede do navegador. No final, forneceremos algumas ideias sobre como ajudar os navegadores a aumentar ainda mais o desempenho de seus aplicativos da web.

visão geral

O navegador da Web moderno foi projetado especificamente para a entrega rápida, eficiente e segura de aplicativos/sites da Web. Com centenas de componentes sendo executados em diferentes camadas, desde gerenciamento de processos e sandbox de segurança até pipelines de GPU, áudio e vídeo e muito mais, o navegador da Web parece mais um sistema operacional do que apenas um aplicativo de software.

O desempenho geral do navegador é determinado por vários componentes grandes: análise, layout, cálculo de estilo, execução de JavaScript e WebAssembly, renderização e, é claro, a pilha de rede .

Os engenheiros geralmente pensam que a pilha de rede é um gargalo. Esse é freqüentemente o caso, já que todos os recursos precisam ser buscados na Internet antes que o restante das etapas seja desbloqueado. Para que a camada de rede seja eficiente, ela precisa desempenhar o papel de mais do que apenas um simples gerenciador de soquete. Ele é apresentado a nós como um mecanismo muito simples para a busca de recursos, mas na verdade é uma plataforma inteira com seus próprios critérios de otimização, APIs e serviços.

Como desenvolvedores web, não precisamos nos preocupar com os pacotes TCP ou UDP individuais, a formatação de solicitações, o armazenamento em cache e tudo mais que está acontecendo. Toda a complexidade é cuidada pelo navegador para que possamos nos concentrar no aplicativo que estamos desenvolvendo. Saber o que está acontecendo sob o capô, no entanto, pode nos ajudar a criar aplicativos mais rápidos e mais seguros.

Em essência, aqui está o que está acontecendo quando o usuário começa a interagir com o navegador:

  • O usuário insere um URL na barra de endereços do navegador
  • Dada a URL de um recurso na web, o navegador inicia verificando seus caches locais e de aplicativos e tenta usar uma cópia local para atender à solicitação.
  • Se o cache não puder ser usado, o navegador pegará o nome de domínio da URL e solicitará o endereço IP do servidor de um DNS . Se o domínio estiver em cache, nenhuma consulta DNS será necessária.
  • O navegador cria um pacote HTTP dizendo que ele solicita uma página da Web localizada no servidor remoto.
  • O pacote é enviado para a camada TCP que adiciona sua própria informação sobre o pacote HTTP. Esta informação é necessária para manter a sessão iniciada.
  • O pacote é então entregue à camada IP cujo trabalho principal é descobrir uma maneira de enviar o pacote do usuário para o servidor remoto. Esta informação também é armazenada no topo do pacote.
  • O pacote é enviado para o servidor remoto.
  • Depois que o pacote é recebido, a resposta é enviada de volta de maneira semelhante.

A especificação W3C Navigation Timing fornece uma API do navegador, bem como a visibilidade dos dados de tempo e desempenho por trás da vida útil de cada solicitação no navegador. Vamos inspecionar os componentes, pois cada um desempenha um papel crítico no fornecimento da melhor experiência do usuário:

Todo o processo de rede é muito complexo e existem muitas camadas diferentes que podem se tornar um gargalo. É por isso que os navegadores se esforçam para melhorar o desempenho, usando várias técnicas para que o impacto de toda a comunicação da rede seja mínimo.

Gerenciamento de soquete

Vamos começar com alguma terminologia:

  • Origem — Um triplo de protocolo de aplicativo, nome de domínio e número de porta (por exemplo, https, www.example.com , 443)
  • Socket pool — um grupo de soquetes pertencentes à mesma origem (todos os principais navegadores limitam o tamanho máximo do pool a 6 sockets)

O JavaScript e o WebAssembly não nos permitem gerenciar o ciclo de vida de soquetes de rede individuais, e isso é bom! Isso não apenas mantém as mãos limpas, mas também permite que o navegador automatize muitas otimizações de desempenho, algumas das quais incluem reutilização de soquete, priorização de solicitação e ligação tardia, negociação de protocolo, imposição de limites de conexão e muitas outras.

Na verdade, os navegadores modernos se esforçam para separar o ciclo de gerenciamento de solicitações do gerenciamento de soquetes. Os soquetes são organizados em pools, que são agrupados por origem, e cada pool impõe seus próprios limites de conexão e restrições de segurança. As solicitações pendentes são enfileiradas, priorizadas e, em seguida, vinculadas a soquetes individuais no pool. A menos que o servidor feche intencionalmente a conexão, o mesmo soquete pode ser reutilizado automaticamente em várias solicitações!

Como a abertura de uma nova conexão TCP tem um custo adicional, a reutilização de conexões introduz benefícios de desempenho excelentes por conta própria. Por padrão, os navegadores usam o mecanismo chamado “keepalive”, que economiza tempo de abrir uma nova conexão com o servidor quando uma solicitação é feita. O tempo médio para abrir uma nova conexão TCP é:

  • Solicitações locais — 23ms
  • Solicitações transcontinentais — 120ms
  • Solicitações intercontinentais — 225ms

Essa arquitetura abre as portas para várias outras oportunidades de otimização. As solicitações podem ser executadas em uma ordem diferente, dependendo de sua prioridade. O navegador pode otimizar a alocação de largura de banda em todos os soquetes ou pode abrir soquetes em antecipação a uma solicitação.

Como mencionei antes, tudo isso é gerenciado pelo navegador e não requer nenhum trabalho do nosso lado. Mas isso não significa necessariamente que não podemos fazer nada para ajudar. Escolher os padrões certos de comunicação de rede, tipo e frequência de transferências, escolha de protocolos e ajuste/otimização de nossa pilha de servidores pode desempenhar um grande papel na melhoria do desempenho geral de um aplicativo.

Alguns navegadores até vão um passo além. Por exemplo, o Chrome pode se autodisciplinar para ficar mais rápido à medida que você o utiliza. Ele aprende com base nos sites visitados e nos padrões de navegação típicos, de modo que ele pode antecipar o comportamento provável do usuário e agir antes que o usuário faça qualquer coisa. O exemplo mais simples é pré-renderizar uma página quando o usuário passa o mouse sobre um link. Se você tiver interesse em saber mais sobre as otimizações do Chrome, confira este capítulo em https://www.igvita.com/posa/high-performance-networking-in-google-chrome/ do livro High-Performance Browser Networking .

Segurança de Rede e Sandboxing

Permitir que o navegador gerencie os soquetes individuais tem outro propósito muito importante: dessa forma, o navegador permite a imposição de um conjunto consistente de restrições de segurança e políticas em recursos de aplicativos não confiáveis. Por exemplo, o navegador não permite o acesso direto da API a soquetes de rede brutos, pois isso permitiria que qualquer aplicativo mal-intencionado fizesse conexões arbitrárias com qualquer host. O navegador também impõe limites de conexão que protegem o servidor, bem como o cliente, do esgotamento de recursos.

O navegador formata todas as solicitações de saída para impor semânticas de protocolo consistentes e bem formadas para proteger o servidor. Da mesma forma, a decodificação de resposta é feita automaticamente para proteger o usuário de servidores mal-intencionados.

Negociação TLS

O Transport Layer Security (TLS) é um protocolo criptográfico que fornece segurança de comunicação em uma rede de computadores. Ele é amplamente usado em muitos aplicativos, um dos quais é a navegação na web. Os sites podem usar o TLS para proteger todas as comunicações entre seus servidores e navegadores da web.

O handshake TLS inteiro consiste nas seguintes etapas:

  1. O cliente envia uma mensagem “Client hello” para o servidor, juntamente com o valor aleatório do cliente e os conjuntos de cifras suportados.
  2. O servidor responde enviando uma mensagem “Servidor hello” para o cliente, juntamente com o valor aleatório do servidor.
  3. O servidor envia seu certificado para autenticação ao cliente e pode solicitar um certificado semelhante ao cliente. O servidor envia a mensagem “Servidor Olá feito”.
  4. Se o servidor solicitou um certificado do cliente, o cliente o envia.
  5. O cliente cria um segredo pré-mestre aleatório e o criptografa com a chave pública do certificado do servidor, enviando o segredo pré-mestre criptografado ao servidor.
  6. O servidor recebe o segredo pré-mestre. O servidor e o cliente geram o segredo mestre e as chaves de sessão com base no segredo pré-mestre.
  7. O cliente envia uma notificação “Alterar especificação de cifra” ao servidor para indicar que o cliente começará a usar as novas chaves de sessão para codificar e criptografar mensagens. O cliente também envia uma mensagem “Cliente finalizado”.
  8. O servidor recebe a “Especificação de cifra de alteração” e alterna seu estado de segurança da camada de registro para criptografia simétrica usando as chaves de sessão. O servidor envia uma mensagem “Server finished” para o cliente.
  9. O cliente e o servidor agora podem trocar dados de aplicativos pelo canal seguro que eles estabeleceram. Todas as mensagens enviadas do cliente para o servidor e vice-versa são criptografadas usando a chave de sessão.

O usuário é avisado caso alguma das verificações falhe — por exemplo, o servidor está usando um certificado autoassinado.

Política de mesma origem

Duas páginas têm a mesma origem se o protocolo, a porta (se houver uma especificada) e o host forem os mesmos para ambas as páginas.

Aqui estão alguns exemplos de recursos que podem ser incorporados de origem cruzada:

  • JavaScript com <script src=”…”></script> . Mensagens de erro para erros de sintaxe só estão disponíveis para scripts de mesma origem
  • CSS com <link rel=”stylesheet” href=”…”> . Devido às regras descontraídas de sintaxe do CSS, CSS de origem cruzada requer um cabeçalho Content-Type correto. Restrições variam de acordo com o navegador
  • Imagens com <img>
  • Arquivos de mídia com <video> e <audio>
  • Plug-ins com <object> , <embed> e <applet>
  • Fontes com @ font-face. Alguns navegadores permitem fontes de origem cruzada, outros exigem fontes de mesma origem.
  • Qualquer coisa com <frame> e <iframe> . Um site pode usar o cabeçalho X-Frame-Options para evitar essa forma de interação de origem cruzada.

A lista acima está longe de estar completa; Seu objetivo é destacar o princípio do “menor privilégio” no trabalho. O navegador expõe apenas as APIs e os recursos necessários para o código do aplicativo: o aplicativo fornece os dados e a URL, e o navegador formata a solicitação e lida com o ciclo de vida completo de cada conexão.

Vale a pena notar que não existe um conceito único de “política de mesma origem”. Em vez disso, há um conjunto de mecanismos relacionados que impõem restrições ao acesso DOM, gerenciamento de estado de cookies e sessões, rede e outros componentes do navegador.

Cache de estado do recurso e do cliente

A melhor e mais rápida solicitação é uma solicitação não feita. Antes de despachar uma solicitação, o navegador verifica automaticamente seu cache de recursos, executa as verificações de validação necessárias e retorna uma cópia local dos recursos, se as condições especificadas forem atendidas. Se um recurso local não estiver disponível no cache, uma solicitação de rede será feita e a resposta será automaticamente colocada no cache para um acesso subseqüente, se tal for permitido.

  • O navegador avalia automaticamente as diretivas de armazenamento em cache em cada recurso
  • O navegador revalida automaticamente os recursos expirados quando possível
  • O navegador gerencia automaticamente o tamanho do cache e o despejo de recursos

Gerenciar um cache de recursos eficiente e otimizado é difícil. Felizmente, o navegador cuida de toda a complexidade em nosso nome, e tudo o que precisamos fazer é garantir que nossos servidores estejam retornando as diretivas de cache apropriadas; para saber mais, consulte Recursos de cache no cliente . Você fornece cabeçalhos de resposta de Cache-Control, ETag e Last-Modified para todos os recursos em suas páginas, certo?

Finalmente, uma função frequentemente negligenciada, mas crítica, do navegador é seu dever de fornecer autenticação, sessão e gerenciamento de cookies. O navegador mantém “jars de cookie” separados para cada origem, fornece APIs de aplicativos e servidores necessários para ler e gravar novos dados de cookies, sessões e autenticação e anexa e processa automaticamente os cabeçalhos HTTP apropriados para automatizar todo o processo em nosso nome.

Um exemplo:

Um exemplo simples, mas ilustrativo, da conveniência de adiar o gerenciamento de estado da sessão para o navegador: uma sessão autenticada pode ser compartilhada em várias guias ou janelas do navegador e vice-versa;uma ação de logoff em uma única guia invalidará as sessões abertas em todas as outras janelas abertas.

APIs e protocolos de aplicativos

Subindo a escada dos serviços de rede fornecidos, finalmente chegamos às APIs e protocolos do aplicativo. Como vimos, as camadas inferiores fornecem uma ampla gama de serviços críticos: gerenciamento de soquete e conexão, processamento de solicitações e respostas, aplicação de várias políticas de segurança, armazenamento em cache e muito mais. Toda vez que iniciamos um HTTP ou um XMLHttpRequest, uma sessão do Server-Sent ou WebSocket de longa duração ou abrimos uma conexão WebRTC, estamos interagindo com alguns ou todos esses serviços subjacentes.

Não há melhor protocolo ou API. Cada aplicativo não trivial exigirá uma combinação de diferentes transportes baseados em uma variedade de requisitos: interação com o cache do navegador, sobrecarga de protocolo, latência de mensagem, confiabilidade, tipo de transferência de dados e muito mais. Alguns protocolos podem oferecer entrega de baixa latência (por exemplo, Eventos enviados pelo servidor, WebSocket), mas podem não atender a outros critérios críticos, como a capacidade de aproveitar o cache do navegador ou oferecer suporte a transferências binárias eficientes em todos os casos.

Algumas coisas que você pode fazer para melhorar o desempenho e a segurança do seu aplicativo Web

  • Sempre use o cabeçalho “Connection: Keep-Alive” em suas solicitações. Os navegadores fazem isso por padrão. Certifique-se de que o servidor use o mesmo mecanismo.
  • Use os cabeçalhos apropriados de Cache-Control, Etag e Last-Modified para que você possa salvar o tempo de download do navegador.
  • Gaste tempo ajustando e otimizando seu servidor web. É aqui que a mágica real pode acontecer! Tenha em mente que o processo é muito específico para cada aplicativo da Web e o tipo de dados que você está transmitindo.
  • Use sempre o TLS! Especialmente se você tiver algum tipo de autenticação em seu aplicativo.
  • Pesquise quais políticas de segurança os navegadores fornecem e imponha-as em seu aplicativo.

Tanto o desempenho quanto a segurança são cidadãos de primeira classe no SessionStack . O motivo pelo qual não podemos nos comprometer com isso é que uma vez que o SessionStack é integrado ao seu aplicativo da Web, ele começa a monitorar tudo, desde alterações do DOM e interação do usuário até solicitações de rede, exceções não tratadas e mensagens de depuração.Todos os dados são transmitidos aos nossos servidores em tempo real, o que permite que você repita os problemas de seus aplicativos da Web como vídeos e veja tudo o que aconteceu com seus usuários. E tudo isso está ocorrendo com latência mínima e sem sobrecarga de desempenho para seu aplicativo.

É por isso que nos esforçamos para empregar todas as dicas acima + mais algumas que discutiremos em um post futuro.

Existe um plano gratuito se você quiser experimentar o SessionStack .

Referências

Este é um artigo traduzido com a autorização do autor. O artigo original pode ser lido em https://blog.sessionstack.com/how-javascript-works-inside-the-networking-layer-how-to-optimize-its-performance-and-security-f71b7414d34c

Autor do post original — Alexander Zlatkov — Co-founder & CEO @SessionStack

--

--

Robisson Oliveira
React Brasil

Senior Cloud Application Architect at Amazon Web Services(AWS). My personal reflections on software development