Renderização do servidor em um ambiente sem servidor

O autor do material, cuja tradução estamos publicando, é um dos fundadores do projeto Webiny - um CMS sem servidor baseado em React, GraphQL e Node.js. Ele diz que o suporte a uma plataforma em nuvem sem servidor com vários inquilinos é um negócio que possui tarefas específicas. Muitos artigos já foram escritos nos quais são discutidas tecnologias padrão para otimizar projetos da web. Entre eles estão a renderização de servidores, o uso de tecnologias avançadas de desenvolvimento de aplicativos Web, várias maneiras de melhorar a criação de aplicativos e muito mais. Este artigo, por um lado, é semelhante aos outros e, por outro, difere deles. O fato é que ele é dedicado à otimização de projetos em execução em um ambiente sem servidor.



Preparação


Para fazer medições que ajudarão a identificar os problemas do projeto, usaremos webpagetest.org . Com a ajuda deste recurso, atenderemos solicitações e coletaremos informações sobre o tempo de execução de várias operações. Isso nos permitirá entender melhor o que os usuários veem e sentem ao trabalhar com o projeto.

Estamos particularmente interessados ​​no indicador "Primeira visualização", ou seja, quanto tempo leva para carregar um site de um usuário que o visita pela primeira vez. Este é um indicador muito importante. O fato é que o cache do navegador é capaz de ocultar muitos gargalos dos projetos da web.

Indicadores que refletem as características do carregamento do site - identificação de problemas


Veja a tabela a seguir.


Análise de indicadores antigos e novos de um projeto web

Aqui, o indicador mais importante pode ser reconhecido como “Hora de iniciar a renderização” - o tempo antes do início da renderização. Se você observar atentamente esse indicador, poderá ver que apenas para começar a renderizar a página, na versão antiga do projeto, demorou quase 2 segundos. A razão para isso está na essência do Aplicativo de Página Única (SPA). Para exibir a página desse aplicativo na tela, você primeiro precisa carregar o pacote JS volumoso (esse estágio de carregamento da página é marcado na figura a seguir como 1). Então esse pacote configurável precisa ser processado no encadeamento principal (2). E somente depois disso, algo pode aparecer na janela do navegador.


(1) Faça o download do pacote JS. (2) Aguardando processamento de pacote configurável no encadeamento principal

No entanto, isso é apenas parte da imagem. Depois que o encadeamento principal processa o pacote configurável JS, ele faz várias solicitações à API do Gateway. Nesta fase do processamento da página, o usuário vê um indicador de carregamento rotativo. A visão não é a mais agradável. No entanto, o usuário ainda não viu nenhum conteúdo da página. Aqui está um storyboard do processo de carregamento da página.


Carregamento da página

Tudo isso sugere que o usuário que visitou esse site experimenta sensações não particularmente agradáveis ​​ao trabalhar com ele. Ou seja, ele é forçado a olhar para uma página em branco por 2 segundos e depois outro segundo - no indicador de download. Este segundo é adicionado ao tempo de preparação da página devido ao fato de que após o carregamento e o processamento das solicitações de API do pacote JS são executadas. Essas consultas são necessárias para carregar os dados e, como resultado, exibir a página final.


Carregamento da página

Se o projeto fosse hospedado em um VPS regular, o tempo necessário para concluir essas solicitações de API seria mais previsível. No entanto, os projetos em execução em um ambiente sem servidor são afetados pelo notório problema de "partida a frio". No caso da plataforma em nuvem Webiny, a situação é ainda pior. Os recursos do AWS Lambda fazem parte do VPC (Virtual Private Cloud). Isso significa que, para cada nova instância dessa função, é necessário inicializar o ENI (Elastic Network Interface, interface de rede elástica). Isso aumenta significativamente o tempo de inicialização a frio das funções.

Aqui estão algumas linhas do tempo para carregar os recursos do AWS Lambda nas VPCs e fora das VPCs.


Análise de carga de função do AWS Lambda dentro da VPC e fora da VPC (imagem obtida aqui )

A partir disso, podemos concluir que, no caso em que a função é iniciada dentro da VPC, isso aumenta em 10 vezes o tempo de partida a frio.

Além disso, aqui mais um fator deve ser levado em consideração - atrasos na transmissão de dados da rede. Sua duração já está incluída no tempo necessário para executar solicitações de API. As solicitações são iniciadas pelo navegador. Portanto, acontece que no momento em que a API responde a essas solicitações, o tempo necessário para obter a solicitação do navegador para a API e o tempo necessário para que a resposta seja obtida da API para o navegador são adicionados. Esses atrasos ocorrem durante cada solicitação.

Tarefas de otimização


Com base na análise acima, formulamos várias tarefas que precisávamos resolver para otimizar o projeto. Aqui estão elas:

  • Melhorando a velocidade de execução de solicitações de API ou reduzindo o número de solicitações de API que bloqueiam a renderização.
  • Reduzir o tamanho do pacote configurável JS ou converter este pacote configurável em recursos que não são necessários para a saída da página.
  • Desbloqueando a linha principal.

Abordagens de Problemas


Aqui estão algumas abordagens para resolver os problemas que consideramos:

  1. Otimização de código com o objetivo de acelerar sua execução. Essa abordagem requer muito esforço, pois tem um alto custo. Os benefícios que podem ser obtidos como resultado dessa otimização são duvidosos.
  2. Aumente a quantidade de RAM disponível para os recursos do AWS Lambda. É fácil, o custo de uma solução desse tipo está entre médio e alto. Apenas pequenos efeitos positivos podem ser esperados a partir da aplicação desta solução.
  3. O uso de alguma outra maneira de resolver o problema. É verdade que naquele momento ainda não sabíamos o que era esse método.

No final, escolhemos o terceiro item desta lista. Nós pensamos assim: “E se absolutamente não precisarmos de chamadas à API? E se pudermos ficar sem o pacote JS? Isso nos permitiria resolver todos os problemas do projeto. ”


A primeira ideia que achamos interessante foi criar um instantâneo HTML da página renderizada e compartilhá-lo com os usuários.

Tentativa mal sucedida


O Webiny Cloud é uma infraestrutura sem servidor baseada no AWS Lambda que suporta sites da Webiny. Nosso sistema pode detectar bots. Quando a solicitação foi concluída pelo bot, essa solicitação é redirecionada para a instância do Puppeteer , que renderiza a página usando o Chrome sem uma interface de usuário. O código HTML pronto da página é enviado ao bot. Isso foi feito principalmente por razões de SEO, devido ao fato de muitos bots não saberem como executar o JavaScript. Decidimos usar a mesma abordagem para preparar páginas destinadas a usuários comuns.


Essa abordagem funciona bem em ambientes que não têm suporte a JavaScript. No entanto, se você tentar fornecer páginas pré-renderizadas para um cliente cujo navegador suporta JS, a página será exibida, mas, após o download dos arquivos JS, os componentes do React simplesmente não saberão onde montá-los. Isso resulta em várias mensagens de erro no console. Como resultado, tal decisão não nos convinha.

Apresentando o SSR


O lado forte da SSR (Server Side Rendering) é que todas as solicitações de API são executadas na rede local. Como eles são processados ​​por um determinado sistema ou função que é executado dentro da VPC, os atrasos que ocorrem ao executar solicitações do navegador para o back-end do recurso não são característicos. Embora neste cenário, o problema de um "início a frio" permaneça.

Uma vantagem adicional do uso do SSR é que fornecemos ao cliente uma versão HTML da página, ao trabalhar com o qual, após o carregamento dos arquivos JS, os componentes do React não apresentam problemas de montagem.

E, finalmente, não precisamos de um pacote JS muito grande. Além disso, podemos fazer sem chamadas de API para exibir a página. Um pacote configurável pode ser carregado de forma assíncrona e isso não bloqueará o encadeamento principal.

Em geral, podemos dizer que a renderização do servidor, ao que parece, deveria ter resolvido a maioria dos nossos problemas.

É assim que a análise do site cuida da aplicação da renderização no servidor.


Métricas do site após aplicar a renderização do servidor

Agora, as solicitações de API não são executadas e a página pode ser vista antes do carregamento do grande pacote JS. Mas se você olhar atentamente para a primeira solicitação, poderá ver que leva quase 2 segundos para obter um documento do servidor. Vamos conversar sobre isso.

Problema com TTFB


Aqui discutimos a métrica TTFB (tempo até o primeiro byte, tempo até o primeiro byte). Aqui estão os detalhes da primeira solicitação.


Detalhes da primeira solicitação

Para processar essa primeira solicitação, precisamos fazer o seguinte: iniciar o servidor Node.js., executar a renderização do servidor, fazer solicitações de API e executar o código JS e, em seguida, retornar o resultado final ao cliente. O problema aqui é que tudo isso, em média, leva 1-2 segundos.

Nosso servidor, que executa a renderização do servidor, precisa fazer todo esse trabalho e somente depois disso poderá transmitir o primeiro byte da resposta ao cliente. Isso leva ao fato de que o navegador tem muito tempo para aguardar o início da resposta à solicitação. Como resultado, agora, para a saída da página, você precisa produzir quase a mesma quantidade de trabalho que antes. A única diferença é que esse trabalho é realizado não no lado do cliente, mas no servidor, no processo de renderização do servidor.

Aqui você pode ter uma pergunta sobre a palavra "servidor". Temos conversado sobre o sistema sem servidor esse tempo todo. De onde veio esse "servidor"? Obviamente, tentamos renderizar a renderização do servidor nas funções do AWS Lambda. Mas aconteceu que esse é um processo que consome muitos recursos (em particular, era necessário aumentar muito a quantidade de memória para obter mais recursos do processador). Além disso, o problema do “arranque a frio”, que já mencionamos, também é adicionado aqui. Como resultado, a solução ideal era usar um servidor Node.js. que carregasse os materiais do site e se envolvesse na renderização deles no lado do servidor.

Vamos voltar às consequências do uso da renderização no servidor. Dê uma olhada no seguinte storyboard. É fácil perceber que não é particularmente diferente daquele obtido no estudo do projeto, que foi renderizado no cliente.


Carregamento da página ao usar a renderização do servidor

O usuário é forçado a olhar para uma página em branco por 2,5 segundos. Isso é triste

Embora olhando para esses resultados, pode-se pensar que não conseguimos absolutamente nada, mas na verdade não é assim. Tivemos um instantâneo em HTML da página contendo tudo o que precisávamos. Esta foto estava pronta para funcionar com o React. Nesse caso, durante o processamento da página no cliente, não era necessário atender a nenhuma solicitação de API. Todos os dados necessários já foram incorporados no HTML.

O único problema foi que a criação desse instantâneo HTML levou muito tempo. Nesse ponto, poderíamos investir mais tempo na otimização da renderização do servidor ou simplesmente armazenar em cache seus resultados e fornecer aos clientes um instantâneo da página a partir de algo como um cache Redis. Nós fizemos exatamente isso.

Armazenando em Cache os Resultados de Renderização do Servidor


Depois que um usuário visita o site da Webiny, primeiro verificamos o cache centralizado do Redis para ver se há um instantâneo HTML da página. Nesse caso, fornecemos ao usuário uma página do cache. Em média, isso reduziu o TTFB para 200-400 ms. Foi após a introdução do cache que começamos a notar melhorias significativas no desempenho do projeto.


Carregamento de página ao usar cache e renderização do lado do servidor

Até o usuário que visita o site pela primeira vez, vê o conteúdo da página em menos de um segundo.

Vejamos como o diagrama em cascata agora se parece.


Métricas do site após aplicar renderização e cache do servidor

A linha vermelha indica um carimbo de hora de 800 ms. É aqui que o conteúdo da página é completamente carregado. Além disso, aqui você pode ver que os pacotes configuráveis ​​JS são carregados em cerca de 1,3 s. Mas isso não afeta o tempo que o usuário precisa para ver a página. Ao mesmo tempo, você não precisa fazer chamadas de API e carregar o thread principal para exibir a página.

Preste atenção ao fato de que o tempo relacionado ao carregamento do pacote JS, execução de solicitações de API e execução de operações no encadeamento principal ainda desempenha um papel importante na preparação da página para o trabalho. Esse investimento de tempo e recursos é necessário para que a página se torne "interativa". Mas isso não tem nenhum papel, primeiro, para os robôs dos mecanismos de pesquisa e, em segundo lugar, para criar a sensação de "carregamento rápido da página" entre os usuários.

Suponha que uma página seja "dinâmica". Por exemplo, ele exibe um link no cabeçalho para acessar a conta do usuário no caso de o usuário que está visualizando a página estar conectado. Após a renderização no servidor, a página de uso geral será enviada ao navegador. Ou seja - aquele que é exibido para usuários que não estão conectados. O título desta página será alterado, refletindo o fato de que o usuário efetuou login, somente após o carregamento do pacote JS e as chamadas de API. Aqui estamos lidando com o indicador TTI (Time To Interactive, tempo para a primeira interatividade).

Algumas semanas depois, descobrimos que nosso servidor proxy não fecha a conexão com o cliente onde é necessário, caso a renderização do servidor tenha sido iniciada como um processo em segundo plano. A correção de literalmente uma linha de código levou ao fato de o indicador TTFB ter sido reduzido para o nível de 50 a 90 ms. Como resultado, o site começou a ser exibido no navegador após cerca de 600 ms.

No entanto, enfrentamos outro problema ...

Problema de invalidação de cache


"Na ciência da computação, existem apenas duas coisas complexas: invalidação de cache e nomeação de entidades".
Phil Carleton

A invalidação do cache é, de fato, uma tarefa muito difícil. Como resolver isso? Primeiramente, você pode atualizar o cache com freqüência, definindo um tempo de armazenamento muito curto para objetos em cache (TTL, Time To Live, life). Às vezes, isso faz com que as páginas sejam carregadas mais lentamente que o normal. Em segundo lugar, você pode criar um mecanismo de invalidação de cache com base em determinados eventos.

No nosso caso, esse problema foi resolvido usando um TTL muito pequeno de 30 segundos. Mas também percebemos a possibilidade de fornecer aos clientes dados obsoletos do cache. No momento em que os clientes recebem esses dados, o cache está sendo atualizado em segundo plano. Graças a isso, nos livramos de problemas, como atrasos e "partida a frio", típicos das funções do AWS Lambda.

Aqui está como isso funciona. Um usuário visita o site Webiny. Estamos verificando o cache HTML. Se houver uma captura de tela da página, nós a entregamos ao usuário. A idade de uma foto pode até demorar alguns dias. Ao passar esse instantâneo antigo para o usuário em algumas centenas de milissegundos, iniciamos simultaneamente a tarefa de criar um novo instantâneo e atualizar o cache. Geralmente, leva alguns segundos para concluir esta tarefa, pois criamos um mecanismo graças ao qual sempre temos um certo número de funções do AWS Lambda que já estão em execução e prontas para funcionar. Portanto, não precisamos, durante a criação de novas imagens, dedicar tempo ao início a frio das funções.

Como resultado, sempre retornamos páginas do cache para os clientes e, quando a idade dos dados em cache atinge 30 segundos, o conteúdo do cache é atualizado.

O cache é definitivamente uma área na qual ainda podemos melhorar alguma coisa. Por exemplo, estamos considerando a possibilidade de atualizar automaticamente o cache quando o usuário publica uma página. No entanto, esse mecanismo de atualização de cache também não é o ideal.

Por exemplo, suponha que a página inicial de um recurso exiba as três postagens mais recentes do blog. Se o cache for atualizado quando uma nova página for publicada, do ponto de vista técnico, somente o cache dessa nova página será gerado após a publicação. O cache da página inicial estará desatualizado.

Ainda estamos procurando maneiras de melhorar o sistema de cache do nosso projeto. Mas até agora, o foco tem sido resolver os problemas de desempenho existentes. Acreditamos que fizemos um bom trabalho em termos de resolução desses problemas.

Sumário


Inicialmente, usamos a renderização do lado do cliente. Então, em média, o usuário poderá ver a página em 3,3 segundos. Agora, esse número caiu para cerca de 600 ms. Também é importante que agora dispensemos o indicador de download.

Para alcançar esse resultado, foi permitido, principalmente, o uso da renderização de servidores. Mas, sem um bom sistema de armazenamento em cache, os cálculos simplesmente são transferidos do cliente para o servidor. E isso leva ao fato de que o tempo necessário para o usuário ver a página não muda muito.

O uso da renderização do servidor tem outra qualidade positiva, não mencionada anteriormente. Estamos falando do fato de facilitar a visualização de páginas em dispositivos móveis fracos. A velocidade de preparação de uma página para exibição em tais dispositivos depende dos recursos modestos de seus processadores. A renderização do servidor permite remover parte da carga deles. Cabe ressaltar que não realizamos um estudo especial sobre esse problema, mas o sistema que possuímos deve ajudar a melhorar a visualização do site em telefones e tablets.

Em geral, podemos dizer que a implementação da renderização do servidor não é uma tarefa fácil. E o fato de usarmos um ambiente sem servidor apenas complica essa tarefa. A solução para nossos problemas exigiu alterações de código, infraestrutura adicional. Precisávamos criar um mecanismo de cache bem projetado. Mas, em troca, recebemos muito bem. O mais importante é que as páginas do nosso site estão agora carregando e se preparando para trabalhar muito mais rápido do que antes. Acreditamos que nossos usuários gostarão.

Caros leitores! Você usa tecnologias de cache e de renderização de servidor para otimizar seus projetos?

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


All Articles