Front-end sofisticado. A arquitetura certa para sites rápidos

Olá, Habr!



Há muito tempo ignoramos o tópico de navegadores, CSS e acessibilidade e decidimos retornar a ele com a tradução do material de visão geral de hoje (original - fevereiro de 2020). Estou especialmente interessado em sua opinião sobre a tecnologia de renderização de servidor mencionada aqui, bem como sobre o quão urgente é a necessidade de um livro completo sobre HTTP / 2 - no entanto, vamos conversar sobre tudo em ordem.



Este post descreve algumas técnicas para acelerar o carregamento de aplicativos front-end e, assim, melhorar a usabilidade.



Vamos dar uma olhada na arquitetura geral do frontend. Como você garante que os ativos críticos carreguem primeiro e maximizar a probabilidade de que esses ativos já acabem no cache?



Não vou me alongar sobre como o back-end deve fornecer recursos, se você precisa fazer sua página na forma de um aplicativo cliente ou como otimizar o tempo de renderização de seu aplicativo.



Visão geral



Vamos dividir o processo de download do aplicativo em três etapas distintas:



  1. Renderização primária - quanto tempo levará para que o usuário veja algo?
  2. Download do aplicativo - quanto tempo leva para que o usuário veja o aplicativo?
  3. – ?




Até o estágio de renderização primária (renderização), o usuário simplesmente não consegue ver nada. Para renderizar uma página, você precisa de pelo menos um documento HTML, mas na maioria dos casos também precisa carregar recursos adicionais, como arquivos CSS e JavaScript. Se disponível, o navegador pode começar a renderizar na tela.



Neste post, estarei usando diagramas em cascata WebPageTest . A cascata de solicitações para seu site será semelhante a esta.







Um conjunto de outros arquivos é carregado junto com o documento HTML e a página é renderizada depois que todos eles estão na memória. Observe que os arquivos CSS são carregados em paralelo, portanto, cada solicitação subsequente não aumenta significativamente o atraso.



Reduzindo o número de solicitações de bloqueio de renderização



Folhas de estilo e (por padrão) elementos de script não permitem que nenhum conteúdo abaixo deles seja exibido.



Existem várias maneiras de corrigir isso:



  • Colocamos tags de script na parte inferior da tag body
  • Carregar scripts de forma assíncrona usando async
  • Escreva pequenos pedaços de JS ou CSS inline se quiser carregá-los de forma síncrona


Evite renderizar cadeias de consulta de bloqueio



Não é apenas o número de solicitações de bloqueio de renderização que pode tornar seu site lento. O que importa é o tamanho de cada um desses recursos que precisam ser baixados e também quando exatamente o navegador detecta que o recurso precisa ser baixado.



Se o navegador ficar ciente da necessidade de baixar o arquivo somente após a conclusão de outra solicitação, você poderá acabar com uma cadeia de solicitações síncronas. Isso pode acontecer por vários motivos:



  • Ter regras @importem CSS
  • Usando fontes da web referenciadas em um arquivo CSS
  • Link de injeção de JavaScript ou tags de script


Considere este exemplo:







Um dos arquivos CSS neste site contém uma regra @importpara carregar uma fonte do Google. Assim, o navegador deve executar as seguintes solicitações uma a uma, nesta ordem:



  1. HTML do documento
  2. CSS do aplicativo
  3. CSS do Google Fonts
  4. Arquivo Woff da fonte do Google (não mostrado em cascata)


Para corrigir isso, primeiro movemos a solicitação de CSS do Google Fonts de @importpara a tag de link no documento HTML. Isso encurtará a cadeia em um elo.



Para aumentar ainda mais a velocidade, incorpore o arquivo CSS do Google Fonts diretamente em seu HTML ou arquivo CSS.



(Lembre-se de que a resposta CSS do Google Fonts depende do agente do usuário. Se você fizer uma solicitação usando o IE8, o CSS se referirá a um arquivo EOT (incorporado pelo OpenType), o IE11 receberá um arquivo woff e os navegadores modernos receberão um woff2. Mas se você estiver satisfeito com isso trabalhar como em navegadores relativamente antigos usando fontes do sistema, você pode simplesmente copiar e colar o conteúdo do arquivo CSS.)



Mesmo depois que a página começa a ser renderizada, o usuário pode não conseguir fazer nada com ela, pois nenhum texto será exibido até que a fonte esteja totalmente carregada. Isso pode ser evitado usando a propriedade de troca de exibição de fonte , que agora é o padrão no Google Fonts.



Às vezes, não é possível se livrar completamente da cadeia de solicitações. Nesses casos, tente usar a tag preloadou preconnect. Por exemplo, o site mostrado acima pode se conectar fonts.googleapis.comantes que a solicitação CSS real seja feita.



Reutilizar conexões de servidor para agilizar solicitações



Normalmente, o estabelecimento de uma nova conexão de servidor requer três passagens de ida e volta entre o navegador e o servidor:



  1. Busca DNS
  2. Estabelecendo uma conexão TCP
  3. Estabelecendo uma conexão SSL


Assim que a conexão for estabelecida, pelo menos mais uma viagem de ida e volta é necessária: envie uma solicitação e baixe uma resposta.



Conforme mostrado na cascata abaixo, as conexões são iniciadas para quatro servidores diferentes: hostgator.com, optimizely.com, googletagmanager.com e googelapis.com.



No entanto , as solicitações subsequentes ao servidor afetado podem reutilizar a conexão existente. Portanto, base.cssou index1.csssão carregados rapidamente, já que também estão localizados em hostgator.com.







Reduzindo o tamanho do arquivo e usando redes de entrega de conteúdo (CDN)



Dois outros fatores que você controla afetam a duração da solicitação, junto com o tamanho do arquivo: o tamanho do recurso e a localização dos seus servidores.



Envie ao usuário a quantidade mínima de dados necessária, além disso, cuide de sua compressão (por exemplo, usando brotli ou gzip).



As redes de distribuição de conteúdo (CDNs) fornecem servidores em uma ampla variedade de locais, portanto, há boas chances de que um deles esteja localizado perto de seus usuários. Você pode conectá-los não ao seu servidor de aplicativos central, mas ao servidor mais próximo no CDN. Assim, o caminho de dados de e para o servidor será reduzido significativamente. Isso é especialmente útil ao trabalhar com recursos estáticos, como CSS, JavaScript e imagens, porque são fáceis de distribuir.



Ignorando a rede com service workers



Os prestadores de serviço permitem que você intercepte solicitações antes que elas entrem na rede. Assim, a primeira renderização pode acontecer quase instantaneamente !







Claro, isso só funciona se você quiser que a rede simplesmente envie uma resposta. Essa resposta já deve estar armazenada em cache, facilitando assim a vida dos usuários ao baixarem novamente o aplicativo.



O service worker mostrado abaixo armazena em cache o HTML e CSS necessários para renderizar a página. Quando recarregado, o aplicativo tenta emitir recursos em cache e, se eles não estiverem disponíveis, ele se volta para a rede como fallback.



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);
   })
 );
});


Para obter mais informações sobre pré-carregamento e armazenamento em cache de recursos usando service workers, consulte este tutorial .



Download de aplicativo



Ok, nosso usuário já viu algo. O que mais ele precisa para poder usar nosso aplicativo?



  1. Carregando o aplicativo (JS e CSS)
  2. Carregando os dados mais importantes de uma página
  3. Baixe dados e imagens adicionais






Observe que não apenas o carregamento de dados pela rede pode retardar a renderização. Quando o código é carregado, o navegador precisa analisá-lo, compilá-lo e executá-lo.



Dividindo o pacote: carregue apenas o código necessário e maximize os acertos do cache.



Ao dividir o pacote, você pode baixar apenas o código de que precisa apenas para esta página, e não baixar o aplicativo inteiro. Ao dividir um pacote, ele pode ser armazenado em cache em partes, mesmo que outras partes do código tenham mudado e precisem ser recarregadas.



Normalmente, o código consiste em três tipos diferentes de arquivos:



  • Código específico para esta página
  • Código de aplicativo compartilhado
  • Módulos de terceiros que raramente mudam (ótimo para cache!)


O Webpack pode dividir automaticamente o código dividido para reduzir o peso geral dos downloads, isso é feito usando optimization.splitChunks . Certifique-se de habilitar o fragmento de tempo de execução para que os hashes dos fragmentos permaneçam estáveis ​​e o cache de longo prazo possa ser usado de maneira útil. Ivan Akulov escreveu um guia detalhado sobre como compartilhar e armazenar em cache o código do Webpack.



A divisão do código específico da página não pode ser feita automaticamente, então você deve identificar trechos que podem ser carregados separadamente. Geralmente, trata-se de uma rota ou conjunto de páginas específico. Use importações dinâmicas para carregar lentamente esse código.



A divisão do pacote resulta em mais solicitações feitas para carregar totalmente seu aplicativo. Mas, se as solicitações forem paralelizadas, esse problema não é grande, principalmente em sites que usam HTTP / 2. Observe as três primeiras consultas nesta cascata:







No entanto, essa cascata também mostra 2 consultas executadas em sequência. Esses fragmentos são necessários apenas para esta página e são carregados dinamicamente por meio de uma chamada import().



Você pode corrigir isso inserindo uma tag preload linkse souber que definitivamente precisará desses fragmentos.







No entanto, como você pode ver, o ganho de velocidade nesse caso pode ser pequeno em comparação com o tempo total de carregamento da página.



Além disso, usar o pré-carregamento às vezes pode ser contraproducente e causar atrasos quando outros arquivos mais importantes são carregados. Confira a postagem de Andy Davis sobre o pré - carregamento de fontes e como bloquear a renderização primária carregando as fontes primeiro e depois o CSS que impede a renderização.



Carregando dados da página



Provavelmente, seu aplicativo foi projetado para exibir algum tipo de dado. Aqui estão algumas dicas sobre como carregar os dados com antecedência e evitar atrasos na renderização.



Não espere pelos pacotes, comece a carregar os dados imediatamente.



Pode haver um caso especial de encadeamento de solicitações sequenciais: você carrega um pacote de aplicativos e este código já solicita os dados da página.



Existem duas maneiras de evitar isso:



  1. Incorporar dados da página em documento HTML
  2. Comece a solicitar dados por meio de um script embutido dentro do documento


A incorporação de dados em HTML garante que seu aplicativo não precise aguardar o carregamento. Também reduz a complexidade geral do aplicativo por não ter que lidar com o estado de carregamento.



No entanto, essa ideia não é tão boa se a obtenção de dados resultar em um atraso significativo na resposta do seu documento, pois também tornará a renderização inicial mais lenta.



Nesse caso, ou ao veicular um documento HTML em cache usando um service worker, você pode incorporar um script embutido no HTML que carregará esses dados. Pode ser fornecido como uma promessa global, como esta:



window.userDataPromise = fetch("/me")


Então, se os dados já estiverem prontos, seu aplicativo pode começar a renderizar imediatamente ou esperar até que esteja pronto.



Ao usar os dois métodos, você precisa saber exatamente quais dados devem ser exibidos na página e mesmo antes de o aplicativo começar a renderizar. Isso geralmente é fácil de fornecer para dados específicos do usuário (nome, notificações ...), mas não é fácil ao lidar com conteúdo específico da página. Tente destacar as páginas mais importantes você mesmo e escreva sua própria lógica para cada uma delas.



Não bloqueie a renderização enquanto espera por dados irrelevantes



Às vezes, a geração de dados paginados requer uma lógica lenta e complexa implementada no back-end. Nesses casos, a capacidade de carregar uma versão simplificada dos dados primeiro é útil, se isso for suficiente para tornar seu aplicativo funcional e interativo.



Por exemplo, uma ferramenta analítica pode primeiro carregar todos os gráficos e depois acompanhá-los com os dados. Assim, o usuário poderá imediatamente ver o diagrama no qual está interessado e você terá tempo para distribuir as solicitações de back-end entre diferentes servidores.







Evite cadeias de consultas de dados sequenciais



Este conselho pode parecer contradizer meu ponto anterior, onde falei sobre adiar o carregamento de dados irrelevantes para uma segunda solicitação. No entanto, evite encadear solicitações consecutivas se uma solicitação subsequente na cadeia não fornecer ao usuário nenhuma informação nova.



Em vez de primeiro perguntar a que usuário está conectado e depois pedir uma lista de grupos aos quais o usuário pertence, retorne a lista de grupos junto com as informações sobre o usuário. Você pode usar GraphQL para isso , mas um ponto de extremidade personalizado user?includeTeams=truetambém é adequado .



Renderização do lado do servidor



Nesse caso, queremos dizer a renderização antecipada do aplicativo no servidor, de modo que uma página HTML completa seja servida como uma resposta a uma solicitação de um documento. Dessa forma, o cliente pode ver a página inteira sem ter que esperar que o código ou dados adicionais sejam carregados!



Como o servidor está enviando apenas HTML estático para o cliente, seu aplicativo ainda está desprovido de interatividade neste estágio. O aplicativo precisa ser carregado, ele precisa executar novamente a lógica de renderização e, em seguida, anexar os ouvintes de evento necessários ao DOM.



Use a renderização do lado do servidor se achar que o conteúdo não interativo é valioso por si só. Além disso, essa abordagem ajuda a armazenar em cache o HTML que foi exibido no servidor e, em seguida, transferi-lo para todos os usuários sem demora quando o documento é solicitado pela primeira vez. Por exemplo, a renderização do lado do servidor é ótima se você estiver renderizando um blog usando React.



Leia este artigo de Michal Janaszek; ele descreve bem como combinar service workers com renderização do lado do servidor.



Próxima página



Em algum ponto, o usuário que trabalha com seu aplicativo precisará ir para a próxima página. Quando a primeira página é aberta, você está no controle de tudo o que acontece no navegador, para que possa se preparar para a próxima interação.



Pré-busca de recursos A



pré-busca do código necessário para exibir a próxima página pode ajudar a evitar atrasos na navegação personalizada. Use tags prefetch linkou webpackPrefetchpara importações dinâmicas:



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


Considere quantos dados do usuário você está usando e qual é a largura de banda, especialmente quando se trata de conexão móvel. É na versão mobile do site que você não pode ser zeloso com o pré-carregamento, e também se o modo de economia de dados está ativado.



Selecione estrategicamente os dados de que seus usuários mais precisam.



Reutilize os dados já carregados.



Armazene localmente os dados Ajax em seu aplicativo para evitar solicitações desnecessárias posteriormente. Se o usuário navegar para a lista de grupos na página Editar Grupo, a transição pode ser feita instantaneamente, reutilizando os dados já selecionados anteriormente.



Observe que isso não funcionará se o seu objeto for editado com frequência por outros usuários e os dados que você carregou ficarem desatualizados rapidamente. Nesses casos, tente primeiro mostrar os dados existentes no modo somente leitura e, enquanto isso, selecione os dados atualizados.



Conclusão



Neste artigo, vimos vários fatores que podem tornar uma página lenta em vários pontos do processo de carregamento. Use ferramentas como Chrome DevTools , WebPageTest e Lighthouse para determinar quais dicas são relevantes para o seu aplicativo.



Na prática, raramente é possível realizar uma otimização abrangente. Determine o que é mais importante para seus usuários e concentre-se nisso.



Enquanto trabalhava neste artigo, percebi que compartilho uma crença profundamente arraigada de que várias consultas são problemas de desempenho ruim. Este 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 esse problema desapareceu com o advento do HTTP / 2 e dos navegadores modernos.



Existem argumentos fortes para dividir as consultas. Fazendo isso, você pode carregar recursos estritamente necessários e fazer melhor uso do conteúdo em cache, já que você só precisa recarregar os arquivos que foram alterados.



All Articles