Aceleração instagram.com. Parte 1

Nos últimos anos, muitas coisas novas apareceram no instagram.com . Muito. Por exemplo - ferramentas de contar histórias, filtros, ferramentas criativas, notificações, mensagens diretas. No entanto, à medida que o projeto cresceu, tudo isso deu um efeito colateral triste: o desempenho do instagram.com começou a declinar. No ano passado, a equipe de desenvolvimento do Instagram fez esforços contínuos para corrigir isso. Isso levou ao fato de que o tempo total de carregamento do feed do Instagram (página de feed) diminuiu quase 50%.



Hoje publicamos uma tradução do primeiro material de uma série de artigos dedicados à história de como o instagram.com foi acelerado.

Sobre como otimizar o desempenho de projetos da web



Melhoria de desempenho no ano passado (feed do Instagram, métrica de exibição concluída, ms.)

Uma das abordagens mais importantes para melhorar o desempenho dos aplicativos Web é priorizar adequadamente os recursos de carregamento e processamento e reduzir o tempo de inatividade do navegador durante o carregamento da página. No nosso caso, muitas dessas otimizações provaram ser mais eficazes do que reduzir o tamanho do código. Como regra, não tivemos reclamações sobre o tamanho do código. Era compacto o suficiente. Suas dimensões começaram a nos incomodar somente depois que muitas pequenas melhorias foram feitas no projeto (também planejamos falar sobre a otimização do tamanho do código). Além disso, essas melhorias tiveram menos impacto no processo de desenvolvimento do projeto. Eles exigiram menos alterações de código e menos refatoração. Como resultado, inicialmente concentramos nossos esforços precisamente nessa área, começando com o pré-carregamento de recursos.

Uma história sobre o pré-carregamento de imagens, o código JavaScript e os materiais necessários para concluir as consultas, bem como onde ter cuidado


O princípio geral de nossas otimizações era informar o navegador o mais rápido possível sobre quais recursos são necessários para carregar a página. Nós, como desenvolvedores do projeto, em muitos casos sabíamos com antecedência o que exatamente seria necessário para isso. Mas o navegador não tinha idéia até que uma certa parte dos materiais da página fosse carregada e processada. Os recursos em questão, na maioria das vezes, incluíam aqueles que são carregados dinamicamente usando JavaScript (por exemplo, outros scripts, imagens, materiais necessários para executar solicitações XHR). O fato é que o navegador não pode detectar esses recursos dependentes até analisar e executar algum código JavaScript.

Em vez de esperar até o próprio navegador encontrar esses recursos, poderíamos dar uma dica, após a qual ele poderia começar a baixá-los imediatamente. Fizemos isso usando os atributos HTML de preload - preload . Parece algo como isto:

 <link rel="preload" href="my-js-file.js" as="script" type="text/javascript" /> 

Usamos dicas semelhantes para dois tipos de recursos em caminhos críticos de carregamento de página. Esse é um código JavaScript carregado dinamicamente e materiais carregados dinamicamente de solicitações de dados GraphQL XHR para dados. Scripts carregados dinamicamente são scripts carregados usando construções do formulário import('...') para rotas específicas do cliente. Mantemos uma lista de correspondência de pontos de entrada do servidor e scripts de rota do cliente. Como resultado, quando nós, no servidor, recebemos uma solicitação para carregar a página, conhecemos os scripts para os quais as rotas do cliente precisam ser baixadas. Como resultado, podemos, ao gerar o código HTML da página, adicionar dicas apropriadas.

Por exemplo, ao trabalhar com o ponto de entrada do FeedPage sabemos que o roteador cliente acabará concluindo uma solicitação para baixar o FeedPageContainer.js . Como resultado, podemos adicionar a seguinte construção ao código da página:

 <link rel="preload" href="/static/FeedPageContainer.js" as="script" type="text/javascript" /> 

Da mesma forma, se soubermos que uma consulta GraphQL está planejada para ser executada para o ponto de entrada de uma página específica, isso significa que precisamos pré-carregar materiais para agilizar a execução dessa consulta. Isso é especialmente importante devido ao fato de que a execução dessas consultas GraphQL às vezes leva muito tempo e a página não pode ser renderizada até que os resultados da consulta sejam retornados. Por esse motivo, precisamos fazer com que o servidor o mais cedo possível se envolva na formação de respostas a essas solicitações.

 <link rel="preload" href="/graphql/query?id=12345" as="fetch" type="application/json" /> 

Alterações nos recursos de carregamento da página são especialmente visíveis em conexões lentas. Ao simular uma conexão 3G rápida (o primeiro gráfico em cascata abaixo, que ilustra a situação quando o pré-carregamento de recursos não é usado), podemos ver que o carregamento do FeedPageContainer.js e a consulta GraphQL associada a ele são iniciados somente após o carregamento do Consumer.js . No entanto, no caso de pré-carregamento, o carregamento do script FeedPageContainer.js e a execução da consulta GraphQL podem começar imediatamente após a página HTML estar disponível. Além disso, reduz o tempo necessário para baixar qualquer script menor que use mecanismos de carregamento lento. Aqui, FeedSidebarContainer.js e ActivityFeedBox.js (que dependem de FeedPageContainer.js ) começam a ser carregados quase imediatamente após o processamento do Consumer.js .


Pré-carregamento não usado


Pré-carregamento usado

Benefícios da priorização da pré-carga


Além de usar o atributo preload para começar a carregar recursos mais rapidamente, o uso desse mecanismo tem outra vantagem. Consiste em aumentar a prioridade de rede do carregamento de script assíncrono. Isso se torna importante ao usar scripts carregados de forma assíncrona em caminhos críticos para carregar páginas, pois, por padrão, eles carregam com baixa prioridade. Como resultado, a prioridade das solicitações de XHR e imagens relacionadas à área da página visível para os usuários será maior do que a dos materiais fora da área de visualização. Mas isso pode levar a situações em que os scripts críticos necessários para renderizar a página são bloqueados ou forçados a compartilhar a largura de banda com outros recursos. Se você estiver interessado, veja uma conta detalhada das prioridades de recursos do Chrome. O uso cuidadoso do mecanismo de pré-carregamento (falaremos mais sobre isso abaixo) fornece ao desenvolvedor um certo nível de controle sobre como o navegador prioriza o processo de carregamento inicial da página. Isso é especialmente verdade naqueles casos em que o desenvolvedor sabe quais recursos são importantes para a exibição correta da página.

Problemas de priorização de pré-carregamento


O problema do pré-carregamento de recursos reside precisamente no fato de oferecer ao desenvolvedor uma alavancagem adicional para influenciar a prioridade do carregamento de recursos. Isso significa que o desenvolvedor tem mais responsabilidade pela priorização adequada. Por exemplo, ao testar um site em regiões nas quais a velocidade das redes móveis e Wi-Fi é muito baixa e nas quais há uma alta porcentagem de perda de pacotes, notamos que a solicitação é executada durante o processamento do <link rel="preload" as="script"> obtém uma prioridade mais alta que a solicitação executada ao processar a <script /> pacotes configuráveis ​​JavaScript usados ​​nos caminhos críticos de renderização de página. Isso leva a um aumento no tempo total de carregamento da página.

A origem desse problema foi como colocamos as tags de pré-carregamento em nossas páginas. Nomeadamente, adicionamos dicas de pré-carregamento apenas para bundles, que fazem parte da página atual, que íamos carregar de forma assíncrona com o roteador do cliente.

 <!--   ,    --> <link rel="preload" href="SomeConsumerRoute.js" as="script" /> <link rel="preload" href="..." as="script" /> ... <!-- ,      --> <script src="Common.js" type="text/javascript"></script> <script src="Consumer.js" type="text/javascript"></script> 

Por exemplo, na página de logout, carregamos SomeConsumerRoute.js para Common.js e Consumer.js , e como os recursos de pré-carregamento são carregados com uma prioridade mais alta, mas não são analisados, isso bloqueia a análise de Common.js e Consumer.js . A equipe de desenvolvimento do Chrome Data Saver encontrou um problema semelhante de pré-carregamento e descreveu sua solução para esse problema. No caso deles, foi decidido sempre colocar construções para pré-carregar recursos assíncronos após as tags <script /> desses recursos que usam esses recursos assíncronos. Decidimos pré-carregar todos os scripts e colocar as construções correspondentes no código na ordem em que elas seriam necessárias. Isso nos dá a oportunidade de começar a pré-carregar todos os recursos de script da página o mais rápido possível. Isso inclui tags para carregar scripts de forma síncrona que não podem ser adicionados ao HTML até que dados específicos do servidor sejam colocados na página. Isso nos permite controlar a ordem de carregamento dos scripts.

Aqui está a marcação que pré-carrega todos os pacotes configuráveis ​​do JavaScript.

 <!--      --> <link rel="preload" href="Common.js" as="script" /> <link rel="preload" href="Consumer.js" as="script" /> <!--   ,    --> <link rel="preload" href="SomeConsumerRoute.js" as="script" /> ... <!-- ,      --> <script src="Common.js" type="text/javascript"></script> <script src="Consumer.js" type="text/javascript"></script> <script src="SomeConsumerRoute.js" type="text/javascript" async></script> 

Pré-carregamento de imagem


Uma das principais áreas de trabalho do instagram.com é o Feed. É uma página de imagem e vídeo que suporta rolagem sem fim. Nós preenchemos esta página assim. Primeiro, faça o download do conjunto inicial de publicações e, à medida que o usuário rola a página, carregue conjuntos adicionais de materiais. No entanto, não queremos que o usuário espere que novos materiais sejam carregados toda vez que ele chegar ao fundo da fita. Como resultado, para facilitar o trabalho com esta página, carregamos novos conjuntos de materiais antes que o usuário chegue ao final da fita.

Na prática, isso, por várias razões, não é uma tarefa fácil:

  • Precisamos fazer o download de materiais que não são visíveis ao usuário, para que eles não usem recursos de rede e processador dos materiais que ele está visualizando.
  • Não gostaríamos de transmitir dados desnecessários pela rede, tentando pré-carregar publicações que o usuário talvez nem visse. Mas, por outro lado, se não pré-carregarmos uma quantidade suficiente de materiais, isso geralmente significa o risco de o usuário "esbarrar" no final da fita.
  • O projeto instagram.com foi desenvolvido para funcionar em vários dispositivos e em telas de vários tamanhos. Como resultado, exibimos as imagens na fita usando o atributo srcset da <img> . Esse atributo permite que o navegador, considerando o tamanho da tela, decida qual resolução de imagem usar. Isso significa que não é tão fácil para nós determinar a resolução das imagens que precisam ser baixadas com antecedência. Além disso, existe o risco de pré-carregar imagens que o navegador não usará.

A abordagem que usamos para resolver esse problema foi criar uma abstração da tarefa de priorização, responsável por enfileirar tarefas assíncronas (nesse caso, são tarefas de pré-carregamento do próximo conjunto de publicações para saída na fita). Uma tarefa semelhante é inicialmente enfileirada com prioridade idle (aqui requestIdleCallback usado). Isso significa que a execução dessa tarefa não será iniciada enquanto o navegador estiver ocupado com qualquer outro trabalho importante. No entanto, se o usuário rolar a página o suficiente para o local onde o conjunto atual de publicações baixadas termina, a prioridade desta tarefa de pré-carregar materiais muda para high . Isso é feito cancelando o retorno de chamada em espera, após o qual o processo de pré-carregamento é iniciado imediatamente.


No início e no meio da fita, a tarefa de pré-carregamento de dados tem prioridade inativa e, no final da fita, a prioridade é alta

Após a conclusão do download dos dados JSON para o próximo lote de publicações, colocamos em fila uma tarefa em segundo plano repetida para pré-carregar imagens deste lote. O pré-carregamento da imagem é realizado na ordem em que as publicações são exibidas no feed e não em paralelo. Isso nos permite priorizar tarefas de carregamento de dados e exibir imagens para publicações mais próximas do local na página que o usuário vê. Para baixar imagens do tamanho correto, usamos um componente de mídia oculto, cujos parâmetros correspondem aos parâmetros da fita atual. Dentro desse componente, existe um elemento <img> que usa o atributo srcset , o mesmo usado para exibir publicações reais no feed. Isso significa que podemos fornecer ao navegador a capacidade de tomar decisões sobre quais imagens pré-carregar. Como resultado, o navegador usará a mesma lógica ao exibir imagens usadas ao pré-carregá-las. Isso também significa que nós, usando um componente de mídia semelhante, podemos pré-carregar imagens para outras áreas do site. Como páginas de perfil de usuário.

O efeito geral das melhorias acima resultou em uma redução de 25% no tempo necessário para o upload de fotos. Estamos falando do período de tempo entre o momento em que o código da publicação é adicionado ao DOM e o momento em que a imagem da publicação é carregada e exibida. Além disso, isso levou a uma redução de 56% no tempo que os usuários, tendo atingido o final do feed, passaram esperando o download de novos materiais.

Caros leitores! Você usa mecanismos de pré-carregamento de dados para otimizar seus projetos da web?


Source: https://habr.com/ru/post/pt468227/


All Articles