Acelerando o frontend. Quando muitas solicitações de servidor são boas

Este artigo descreve alguns métodos para acelerar o carregamento de aplicativos front-end para implementar uma interface de usuário rápida e responsiva.

Discutiremos a arquitetura geral do front-end, como pré-carregar os recursos necessários e aumentar a probabilidade de eles estarem no cache. Discutiremos um pouco sobre como fornecer recursos do back-end e quando é possível limitar-nos a páginas estáticas, em vez de um aplicativo cliente interativo.

O processo de download é dividido em três etapas. Para cada estágio, formulamos estratégias gerais para aumentar a produtividade:

  1. Renderização inicial : quanto tempo leva para o usuário ver pelo menos algo
    • Reduzir solicitações de bloqueio de renderização
    • Evite cadeias seqüenciais
    • Reutilizar conexões do servidor
    • Trabalhadores de serviço para renderização instantânea
  2. : ,
    • . .
    • ,


  3. :
    • ,


Até a renderização inicial, o usuário não vê nada na tela. O que precisamos para esta renderização? No mínimo, faça upload de um documento HTML e, na maioria dos casos, recursos adicionais, como arquivos CSS e JavaScript. Uma vez disponíveis, o navegador pode iniciar algum tipo de renderização. Os

gráficos WebPageTest são fornecidos neste artigo . A sequência de consulta para o seu site provavelmente terá algo parecido com isto.



O documento HTML carrega vários arquivos adicionais e a página é renderizada após o download. Observe que os arquivos CSS são carregados em paralelo entre si, portanto, cada solicitação adicional não adiciona atraso significativo.

(Nota: na captura de tela, gov.uk é um exemplo em que o HTTP / 2 agora está ativadopara que o domínio do recurso possa reutilizar uma conexão existente. Veja abaixo as conexões do servidor.)

Reduzir solicitações de bloqueio de renderização


Folhas de estilo e scripts (por padrão) bloqueiam a renderização de qualquer conteúdo abaixo deles.

Existem várias opções para corrigir isso:

  • Mover tags de script para a parte inferior do corpo
  • Faça o download de scripts no modo assíncrono usando async
  • Se JS ou CSS devem ser carregados sequencialmente, é melhor incorporá-los com pequenos trechos

Evite conversas com solicitações sequenciais que bloqueiam a renderização


O atraso na renderização do site não está necessariamente associado a um grande número de solicitações que bloqueiam a renderização. Mais importante é o tamanho de cada recurso, bem como a hora de início do seu download. Ou seja, o momento em que o navegador percebe subitamente que esse recurso precisa ser baixado.

Se o navegador detectar a necessidade de baixar o arquivo somente depois de concluir outra solicitação, haverá uma cadeia de solicitações. Pode se formar por vários motivos:

  • Regras @importCSS
  • Fontes da Web referenciadas pelo arquivo CSS
  • Tags de script ou JavaScript para download

Veja este exemplo:



Um dos arquivos CSS deste site carrega a fonte do Google através da regra @import. Isso significa que o navegador deve se revezar na execução dos seguintes pedidos:

  1. Documento HTML
  2. Aplicações CSS
  3. CSS para fontes do Google
  4. Arquivo Woff de fonte do Google (não mostrado no diagrama)

Para corrigir isso, primeiro mova a solicitação CSS do Google Fonts da tag @importpara o link no documento HTML. Então reduzimos a corrente em um link.

Para acelerar ainda mais as coisas, incorpore o CSS das fontes do Google diretamente ao seu arquivo HTML ou CSS.

(Lembre-se de que a resposta CSS do servidor do Google Fonts depende da linha do agente do usuário. Se você fizer uma solicitação usando o IE8, o CSS consultará o arquivo EOT, o IE11 receberá o arquivo woff e os navegadores modernos receberão o arquivo woff2. Se você concorda que Se os navegadores antigos ficarem limitados às fontes do sistema, você pode simplesmente copiar e colar o conteúdo do arquivo CSS para si mesmo).

Mesmo após o início da renderização, é improvável que o usuário consiga interagir com a página, porque a fonte precisa ser carregada para exibir o texto. Esse é um atraso de rede adicional que eu gostaria de evitar. O parâmetro swap é útil aqui , permite usá-lo font-displaycom o Google Fonts e armazenar fontes localmente.

Às vezes, a cadeia de consulta não pode ser resolvida. Nesses casos, convém considerar a tag de pré-carregamento ou pré-conexão . Por exemplo, o site no exemplo acima pode se conectar fonts.googleapis.comantes que a solicitação CSS real chegue.

Reutilizando conexões do servidor para acelerar solicitações


Para estabelecer uma nova conexão com o servidor, geralmente são necessárias três trocas de pacotes entre o navegador e o servidor:

  1. Pesquisa de DNS
  2. Estabelecer uma conexão TCP
  3. Estabelecer uma conexão SSL

Depois que a conexão é estabelecida, pelo menos mais uma troca de pacotes é necessária para enviar uma solicitação e receber uma resposta.

O gráfico abaixo mostra que iniciar uma ligação com quatro servidores diferentes: hostgator.com, optimizely.com, googletagmanager.com, e googelapis.com.

No entanto, solicitações subsequentes do servidor podem reutilizar uma conexão existente . O download base.csstanto index1.cssacontece mais rápido porque eles estão localizados no mesmo servidor hostgator.comcom o qual uma conexão já foi estabelecida.



Reduza o tamanho do arquivo e use CDN


Você controla dois fatores que afetam o tempo de execução da consulta: o tamanho dos arquivos de recursos e o local dos servidores.

Envie o mínimo possível de dados para o usuário e verifique se eles estão compactados (por exemplo, usando brotli ou gzip).

As redes de entrega de conteúdo (CDNs) possuem servidores em todo o mundo. Em vez de se conectar a um servidor central, um usuário pode se conectar a um servidor CDN mais próximo. Assim, a troca de pacotes será muito mais rápida. Isso é especialmente adequado para recursos estáticos, como CSS, JavaScript e imagens, pois são fáceis de distribuir via CDN.

Elimine a latência da rede com trabalhadores de serviço


Os técnicos de serviço permitem interceptar solicitações antes de enviá-las para a rede. Isso significa que a resposta vem quase instantaneamente !



Obviamente, isso só funciona se você realmente não precisar receber dados da rede. A resposta já deve estar em cache, para que o benefício apareça apenas no segundo download do aplicativo.

O responsável pelo serviço abaixo armazena em cache o HTML e o CSS necessários para renderizar a página. Quando o aplicativo é carregado novamente, ele tenta emitir os próprios recursos em cache - e acessa a rede somente se eles não estiverem disponíveis.

self.addEventListener("install", async e => {
 caches.open("v1").then(function (cache) {
   return cache.addAll(["/app", "/app.css"]);
 });
});

self.addEventListener("fetch", event => {
 event.respondWith(
   caches.match(event.request).then(cachedResponse => {
     return cachedResponse || fetch(event.request);
   })
 );
});

Em este guia, explicado em detalhes sobre o uso de workers` serviço para recursos de pré-carga e cache.

Baixar aplicativo


Então, o usuário vê algo na tela. Que etapas adicionais são necessárias para ele usar o aplicativo?

  1. Faça o download do código do aplicativo (JS e CSS)
  2. Faça o download dos dados necessários para a página
  3. Baixe dados e imagens adicionais



Observe que não apenas o download de dados da rede pode atrasar a renderização. Depois que seu código é carregado, o navegador deve analisá-lo, compilá-lo e executá-lo.

Faça o download apenas do código necessário e maximize o número de ocorrências no cache


“Quebrar um pacote” significa baixar apenas o código necessário para a página atual, não o aplicativo inteiro. Isso também significa que partes do pacote podem ser armazenadas em cache, mesmo que outras partes tenham sido alteradas e precisem ser recarregadas.

Como regra, o código é dividido nas seguintes partes:

  • Código para uma página específica (específica da página)
  • Código de aplicação comum
  • Módulos de terceiros que raramente mudam (ótimo para armazenamento em cache!)

O Webpack pode fazer essa otimização automaticamente, quebrar o código e reduzir o peso geral da carga. O código é dividido em pedaços usando o objeto optimization.splitChunks . Separe o tempo de execução (tempo de execução) em um arquivo separado: dessa forma, você pode se beneficiar do cache de longo prazo. Ivan Akulov escreveu um guia detalhado sobre como dividir um pacote em arquivos separados e armazenar em cache no Webpack .

Não é possível alocar automaticamente o código para uma página específica. Você deve identificar manualmente as partes que podem ser baixadas separadamente. Geralmente, esse é um caminho ou conjunto de páginas específico. Use importações dinâmicas para carregar lentamente esse código.

Dividir o pacote geral em partes aumentará o número de solicitações para fazer o download do seu aplicativo. Mas isso não é um grande problema se as solicitações forem executadas em paralelo, especialmente se o site for carregado usando o protocolo HTTP / 2. Você pode ver isso nas três primeiras consultas no diagrama a seguir:



No entanto, duas consultas consecutivas também são visíveis no diagrama. Esses fragmentos são necessários apenas para esta página específica e são carregados dinamicamente import().

Você pode tentar corrigir o problema inserindo uma tag preload preload .



Mas vemos que o tempo total de carregamento da página aumentou.

Às vezes, o pré-carregamento de recursos é contraproducente, pois atrasa o carregamento de arquivos mais importantes. LerArtigo de Andy Davis sobre pré-carregamento de fontes e como esse procedimento bloqueia o início da renderização da página.

Carregando dados para uma página


Seu aplicativo provavelmente deve mostrar alguns dados. Aqui estão algumas dicas que você pode usar para baixar esses dados mais cedo, sem atrasos desnecessários na renderização.

Não espere o download completo do pacote antes de começar a baixar os dados


Aqui está um caso especial de uma cadeia de solicitações sequenciais: você baixa o pacote de aplicativos inteiro e, em seguida, esse código solicita os dados necessários para a página.

Existem duas maneiras de evitar isso:

  1. Incorporar dados em um documento HTML
  2. Execute uma solicitação de dados usando o script interno dentro do documento

A incorporação dos dados em HTML garante que o aplicativo não espere o carregamento. Também reduz a complexidade do sistema, pois você não precisa lidar com o status de inicialização.

No entanto, essa não é uma boa ideia se essa técnica atrasar a renderização inicial.

Nesse caso, além de enviar um documento HTML em cache por meio de um trabalhador de serviço, você pode usar o script interno para baixar esses dados como alternativa. Você pode disponibilizá-lo como um objeto global, aqui está uma promessa:

window.userDataPromise = fetch("/me")

Se os dados estiverem prontos e em tal situação, o aplicativo poderá iniciar imediatamente a renderização ou aguardar até que esteja pronto.

Ao usar os dois métodos, você precisa saber com antecedência quais dados a página carregará antes que o aplicativo inicie a renderização. Isso geralmente é óbvio para dados relacionados ao usuário (nome de usuário, notificações etc.), mas é mais difícil com o conteúdo específico de uma página específica. Talvez faça sentido destacar as páginas mais importantes e escrever sua própria lógica para elas.

Não bloqueie a renderização enquanto aguarda dados irrelevantes


Às vezes, para gerar dados, você precisa executar uma lógica complexa lenta no back-end. Nesses casos, você pode tentar baixar uma versão mais simples dos dados primeiro, se isso for suficiente para tornar o aplicativo funcional e interativo.

Por exemplo, uma ferramenta de análise pode primeiro fazer o download de uma lista de todos os gráficos antes de carregar os dados. Isso permite que o usuário procure imediatamente o diagrama de seu interesse e também ajuda a distribuir solicitações de back-end para diferentes servidores.



Evite consultas consecutivas de dados


Isso pode contradizer o parágrafo anterior, de que é melhor divulgar dados não essenciais em uma solicitação separada. Portanto, deve ser esclarecido: evite cadeias com solicitações de dados sequenciais, se cada solicitação concluída não levar ao fato de que o usuário recebe mais informações .

Em vez de primeiro consultar qual usuário está conectado e solicitar uma lista de seus grupos, retorne imediatamente a lista de grupos junto com as informações do usuário na primeira consulta. Você pode usar o GraphQL para isso , mas o terminal user?includeTeams=truetambém funciona bem.

Renderização do lado do servidor


Renderização no servidor significa pré-renderizar o aplicativo, para que um HTML de página inteira seja retornado mediante solicitação do cliente. O cliente vê a página completamente renderizada, sem aguardar o carregamento de código ou dados adicionais!

Como o servidor envia ao cliente apenas HTML estático, o aplicativo não é interativo neste momento. Você precisa fazer o download do aplicativo em si, iniciar a lógica de renderização e conectar os ouvintes de eventos necessários ao DOM.

Use a renderização no servidor se a visualização de conteúdo não interativo for valiosa por si só. Também é bom armazenar em cache o HTML renderizado no servidor e devolvê-lo imediatamente a todos os usuários sem demora. Por exemplo, a renderização do lado do servidor é excelente ao usar o React para exibir postagens do blog.

ATEste artigo de Mikhail Yanashek descreve como combinar trabalhadores de serviço e renderização do lado do servidor.

Próxima página


Em algum momento, o usuário está prestes a pressionar um botão e ir para a próxima página. A partir do momento em que você abre a página inicial, você controla o que acontece no navegador para poder se preparar para a próxima interação.

Pré-carregamento de recursos


Se você pré-carregar o código necessário para a próxima página, o atraso desaparecerá quando o usuário iniciar a navegação. Use tags de pré-busca ou webpackPrefetchpara importação dinâmica:

import(
    /* webpackPrefetch: true, webpackChunkName: "todo-list" */ "./TodoList"
)

Considere o tipo de carga que você coloca no usuário em termos de tráfego e largura de banda, especialmente se ele estiver conectado por uma conexão móvel. Se uma pessoa baixou a versão móvel do site e o modo de armazenamento de dados está ativo, é razoável pré-carregar menos agressivamente.

Considere estrategicamente quais partes do aplicativo o usuário precisará antes.

Reutilização de dados já baixados


Armazene dados em cache localmente no seu aplicativo e use-os para evitar solicitações futuras. Se o usuário sair da lista de seus grupos para a página "Editar grupo", você poderá fazer a transição instantaneamente, reutilizando dados baixados anteriormente sobre o grupo.

Observe que isso não funcionará se o objeto for frequentemente editado por outros usuários e os dados baixados puderem estar desatualizados. Nesses casos, há uma opção para mostrar primeiro os dados somente leitura existentes enquanto executa simultaneamente uma solicitação de dados atualizados.

Conclusão


Este artigo lista vários fatores que podem tornar a página mais lenta em diferentes estágios do processo de carregamento. Ferramentas como o Chrome DevTools , o WebPageTest e o Lighthouse ajudarão você a descobrir quais desses fatores afetam seu aplicativo.

Na prática, raramente a otimização está indo imediatamente em todas as direções. Precisamos descobrir o que tem maior impacto sobre os usuários e focar nisso.

Enquanto escrevia o artigo, percebi uma coisa importante: eu acreditava firmemente que muitas solicitações individuais de servidor eram ruins para o desempenho. Esse era o caso no passado, quando cada solicitação exigia uma conexão separada e os navegadores permitiam apenas algumas conexões por domínio. Mas com HTTP / 2 e navegadores modernos, esse não é mais o caso.

Existem bons argumentos a favor de dividir o aplicativo em partes (com consultas multiplicadas). Isso permite que você baixe apenas os recursos necessários e é melhor usar o cache, pois apenas os arquivos alterados precisarão ser recarregados.

All Articles