Como a eficiência da rede pode ser aprimorada com a redução do número de chamadas ao servidor?

Troopers Legacy
Troopers-Legacy
Published in
14 min readOct 2, 2023

É sabido que as solicitações do servidor são uma das tarefas mais caras nos aplicativos da Web. Ainda hoje, isso continua sendo um desafio significativo para desenvolvedores e engenheiros que buscam criar aplicativos altamente eficientes.

Apesar dos últimos avanços no campo da comunicação que melhoraram significativamente a rede, como 5G, Wi-Fi 6 e assim por diante, ainda é necessário ter cuidado com o tempo de resposta do servidor e o consumo de recursos. Na verdade, de acordo com a natureza da solução, esse sempre será um requisito crucial. Os aplicativos em tempo real, por exemplo, podem ser extremamente sensíveis à latência da rede. Além disso, outros fatores relevantes nos obrigam a maximizar a eficiência da rede na maior medida possível, conforme descrito abaixo:

  • Energia verde e codificação verde

Hoje, mais do que nunca, as mudanças climáticas estão em foco e, sim, a maneira como codificamos faz parte delas. A execução de solicitações desnecessárias aumenta a carga do servidor e da própria infraestrutura de rede (roteadores, amplificadores, switches etc.), o que resulta em maior consumo de energia. É importante ter em mente que seu código requer energia elétrica para funcionar. Portanto, é necessário minimizar o consumo de energia do software, limitando assim o possível impacto ambiental e evitando solicitações desnecessárias.

  • Planos de dados móveis limitados

Se alguém estiver acessando seu site com um plano de dados móveis limitado, cada solicitação de rede desnecessária é um desperdício de dinheiro.

  • Redes lentas

Atualmente, não é difícil encontrar pessoas que estejam enfrentando problemas com conexões problemáticas em termos de velocidade e estabilidade. Uma parcela significativa da população mundial ainda depende do que seria considerado “conexões ruins” (de acordo com Uma visão geral global dos preços e da velocidade da Internet).). Nesse caso, é desejável manter as solicitações do servidor tão eficientes quanto possível em termos de quantidade e tamanho da carga útil.

  • Experiência do usuário

As páginas da Web não serão carregadas até que todos os seus recursos essenciais tenham sido completamente baixados, o que pode levar a longos períodos de espera e, consequentemente, à perda de atenção dos usuários.

Esses são apenas alguns exemplos, outros cenários também são possíveis. Entre todas essas possibilidades, este artigo se concentrará em fornecer algumas dicas de software para otimizar a comunicação cliente-servidor. Especificamente, ele abordará a transferência de dados (tamanho e velocidade), evitando solicitações desnecessárias e otimizando as existentes. A ideia seria manter a infraestrutura de rede atual inalterada e aumentar seu desempenho e qualidade gerais seguindo estas dicas.

HTTP: a maneira usual e não tão otimizada de conectar cliente e servidor

Com certeza, a transferência de dados é, sem dúvida, um aspecto essencial quando se discute a eficiência da conexão, se não for o mais importante. Isso afeta diretamente todos os pontos discutidos anteriormente. Para aprimorar o processo de transferência de dados, geralmente é necessário atuar no HTTP — Hypertext Transfer Protocol, que é um dos padrões mais usados para carregar páginas da Web e trocar informações entre um cliente e um servidor.

Em termos de HTTP, há várias abordagens para otimizar a comunicação entre o servidor e o cliente, normalmente a mais comum seria:

  1. Minificação de recursos;
  2. Compactação de arquivos;
  3. Redução do número de chamadas ao servidor e otimização de solicitações;
  4. Formato das imagens;
  5. Carregamento lento;
  6. Armazenamento em cache;
  7. CDN — Rede de distribuição de conteúdo;

Cada um desses exemplos merece um artigo dedicado para ser abordado adequadamente. Dessa forma, como mencionado anteriormente, este conteúdo se restringirá apenas ao terceiro, que fornece algumas respostas possíveis para a pergunta: Como a eficiência da rede pode ser aprimorada com a redução do número de chamadas ao servidor?

O principal problema do HTTP 1.1 (a versão mais comumente usada) é sua ineficiência. As solicitações HTTP podem ser lentas e caras. O diagrama a seguir mostra uma visão geral simplificada das chamadas HTTP típicas.

Figura 1 — O HTTP requer várias viagens de ida e volta antes de iniciar o fornecimento de conteúdo.

Toda chamada HTTP é composta de uma solicitação e uma resposta. Antes de receber qualquer dado, o cliente precisa negociar cada solicitação com o servidor usando um processo chamado handshake, que não está descrito no diagrama acima. Esta é uma maneira divertida de explicar como funciona o handshaking HTTP: Como funciona o HTTPS”.

Portanto, tudo isso acaba consumindo uma parte considerável do tempo total, que é a duração desde a solicitação inicial do processo até o início da transmissão de dados. Além disso, há uma latência de rede associada à infraestrutura, que pode afetar diretamente o tempo de resposta. Infelizmente, o protocolo HTTP é bastante complexo para ser totalmente discutido neste artigo. Você pode consultar o artigo da MDN para obter uma visão geral conceitual mais detalhada: Uma visão geral do HTTP.”
Nesse ponto, há uma clara necessidade de cortar solicitações desnecessárias e otimizar as existentes. No entanto, antes de entrar nesse assunto, vale a pena falar um pouco sobre o cache HTTP, uma das primeiras linhas de defesa contra solicitações desnecessárias. Em geral, o cache HTTP é uma maneira eficaz de melhorar o desempenho da carga porque reduz as solicitações de rede desnecessárias. Ele é compatível com todos os navegadores e não exige muito esforço para ser configurado. Como o nome sugere, o navegador consultará seu cache antes de iniciar uma nova solicitação para verificar se há uma resposta válida em cache que possa ser usada para atender à solicitação. Se houver uma correspondência, a resposta será lida do cache, eliminando a latência da rede e o custo dos dados incorridos pela transferência.

Figura 2 — O cache HTTP é a porta de entrada para os desenvolvedores melhorarem o desempenho dos aplicativos e a experiência do usuário final. Adaptado de: O que é o cache HTTP e como ele funciona?

O cache HTTP é apenas uma parte do que seria a solução ideal e não elimina a necessidade de projetar um mecanismo de comunicação eficiente entre o servidor e o cliente. De fato, há um consenso de que a maneira mais bem-sucedida de otimizar um aplicativo em termos de solicitações HTTP é no nível do código.

Indo além do cache HTTP

Excluindo os problemas do servidor, talvez o principal motivo da dificuldade com o tempo de resposta seja a maneira como os próprios desenvolvedores projetam as chamadas. Do ponto de vista do código, há alguns padrões e truques de design simples que podem ser bastante úteis para melhorar o tempo de execução. Vamos explorar algumas delas a seguir.

Os exemplos baseados em código utilizam Node.js + Knex no lado do servidor e JavaScript no lado do cliente para demonstrar sua implementação prática aplicada aos dados do painel solar. No entanto, os mesmos conceitos também podem ser aplicados a outras tecnologias. A Figura 2 contém o conjunto de dados usado para demonstrar os exemplos, que consiste em três linhas de dados de painéis solares.

Figura 2 — Conjunto de dados de painéis solares.

Reutilizar dados locais sempre que possível.

Esse princípio nos diz para reutilizar os dados disponíveis sempre que possível, em vez de fazer uma nova solicitação para obter os mesmos dados. Isso fica claro para operações de edição e exclusão. Por exemplo, vamos imaginar que a segunda linha foi editada, alterando seu valor de potência para 14,00, de modo que o conjunto de dados ficaria assim:

Figura 3 — Conjunto de dados editado: valor atualizado para “power” na segunda linha.

Nesse caso, o cliente precisaria apenas aguardar um status 204, indicando que a operação de edição foi bem-sucedida porque todos os dados necessários já estão disponíveis no cliente.

Figura 4 — Talvez retornar um simples status 204 seja suficiente ao editar um registro.

Ao fazer isso no lado do servidor, obtemos respostas HTTP mais leves e ainda mais rápidas. Por fim, o aplicativo cliente só precisa atualizar seus dados locais com o registro mais recente.

Figura 5 — O aplicativo cliente só precisa substituir seus dados locais pelo valor mais recente.

O mesmo princípio pode ser aplicado às operações de exclusão. Nesse caso, o servidor enviaria um status 204 e o cliente removeria o registro específico do seu conjunto de dados. Na maior parte do processo de criação, os dados necessários são gerados no lado do cliente. Normalmente, apenas uma pequena quantidade de informações, como a chave primária e outros campos necessários, é gerada no servidor. Dessa forma, a maioria dos dados pode ser reutilizada diretamente pelo cliente. Por exemplo, vamos considerar a criação de um novo registro de painel. O serviço poderia ser o seguinte:

Figura 6 — Ao criar um novo registro, basta enviar de volta os campos recém-gerados que não estão disponíveis no cliente do aplicativo.

Criar valores constantes no cliente em vez de procurá-los no servidor.

Esse ponto pode parecer bastante controverso e, de fato, é. A ideia é manter valores constantes ou pouco mutáveis no cliente em vez de recuperá-los do servidor. A principal desvantagem dessa abordagem é que, quando um valor é alterado, é necessário atualizá-lo em dois locais para manter a sincronização, tanto no cliente quanto no servidor. Isso pode ser inviável para determinados aplicativos, mas, se não for, pode ajudar a reduzir a sobrecarga geral de solicitações. Por exemplo, vamos supor que existam diferentes tipos de painéis disponíveis:

Figura 7 — Tipos de painéis solares.

A estrutura de dados mostrada na Figura 7 poderia ser replicada no cliente, evitando a necessidade de uma solicitação adicional para buscá-la no servidor.

Figura 8 — Os valores constantes podem ser definidos diretamente no cliente para evitar espera.

Então, ao apresentar os tipos de painel ao usuário, não é mais necessário esperar por eles. Essa dica ajuda a evitar solicitações desnecessárias para buscar pequenas informações.

Agrupamento de solicitações HTTP

Essa dica visa acelerar o tempo de carregamento geral do aplicativo agrupando solicitações. Como mencionado anteriormente, cada solicitação HTTP requer uma viagem de ida e volta (Figura 1). Consequentemente, quanto mais solicitações o aplicativo tiver, mais tempo ele levará. No entanto, se for possível agrupar essas solicitações em uma única, o tempo necessário poderá ser reduzido significativamente. Vamos imaginar uma página da Web usada para criar kits solares, que são compostos por inversores, transformadores, itens opcionais e os próprios painéis. Os seguintes recursos são necessários para preencher a página de construção de kits:

  • Inversores;
  • Transformers;
  • Itens adicionais;
  • Painéis.

Cada um dos elementos acima precisa ser obtido de pontos de extremidade específicos; inicialmente, ele acabaria realizando quatro solicitações:

Figura 9 — Principalmente devido às viagens de ida e volta do HTTP, várias solicitações independentes podem levar mais tempo para serem feitas em comparação com solicitações agrupadas ou únicas.

Nesse exemplo hipotético, todas as solicitações levaram um total de 1100 ms. Para melhorar esse registro de data e hora, uma solução viável seria consolidar todas as solicitações em um único ponto de extremidade, que podemos chamar de “kit-resource”, conforme mostrado na Figura 10.

Figura 10 — De acordo com a arquitetura do servidor, solicitações agrupadas ou individuais podem economizar um tempo precioso.

Dessa vez, foram necessários apenas 650 ms, em vez dos 1100 ms exigidos por várias solicitações. É importante ter em mente que os números usados nesses exemplos são irreais; eles são usados apenas para exemplificar. O que importa é a ideia de que, em geral, o agrupamento de solicitações pode economizar tempo de carregamento.

A ideia de agrupar solicitações funciona bem quando estamos falando de uma única API, mas para micros serviços, isso não é necessariamente verdade. É necessária uma análise para determinar se vale a pena agrupá-los em um API Gateway antes de enviá-los ao cliente. Caso contrário, é inevitável que haja várias solicitações. Alguns leitores podem argumentar sobre o GraphQL e tecnologias semelhantes como alternativas ao agrupamento de solicitações. No entanto, essa é uma discussão completamente diferente. Como o objetivo deste artigo é abordar apenas os serviços Restful, discutir o GraphQL estaria fora do escopo.

Paginação de dados

A paginação de dados não é um conceito novo para a maioria dos desenvolvedores, e talvez já esteja bem definido em suas mentes. No entanto, é interessante mencionar essa abordagem. É inevitável, mais cedo ou mais tarde seu servidor precisará dela para manter sua operação saudável. É claro que é altamente recomendável projetar aplicativos com paginação de dados desde o início.

Figura 11 — A paginação de dados reduz consideravelmente a sobrecarga do servidor e do cliente.

Ao paginar os dados, as solicitações se tornam baseadas na demanda. Em outras palavras, o bloco atual só é obtido do servidor quando é realmente necessário. Esse comportamento traz vários benefícios para o servidor e o cliente, evitando a sobrecarga e reduzindo o tempo de carregamento. Observe que, desta vez, não estamos falando em cortar o número de solicitações, pelo contrário, estamos tentando melhorar o desempenho geral distribuindo uniformemente os dados ao longo do tempo, de acordo com as necessidades do cliente.

Carregamento lento

Essa técnica é bastante eficiente e se baseia no mesmo princípio básico da anterior: paginação de dados. A ideia aqui é obter solicitações mais leves, eliminando cargas desnecessárias. Uma página pode consistir em vários recursos, como imagens. Ao visitar essa página, o usuário pode não ter tido a intenção de rolar a página até o final para ver todas essas imagens. Portanto, acionar solicitações para recuperar esses dados seria uma completa perda de tempo. O carregamento lento entra em ação para resolver esse problema, “paginando” os próprios recursos da página, incluindo CSS, HTML e JavaScript. Em vez de carregar o aplicativo inteiro de uma vez, ele pode ser dividido em “partes” para obter solicitações mais rápidas. As duas figuras a seguir ilustram como isso funciona de forma simples.

Figura 12 — Sem o carregamento lento, o aplicativo carrega todos os seus recursos, mesmo quando eles não são necessários para o contexto atual.

Na Figura 12, quando um usuário navega diretamente para a página inicial usando o caminho “/home”, o aplicativo carrega todos os seus ativos, incluindo arquivos de outras páginas. Mesmo que o usuário não visite a “página de vendas”, ela levará mais tempo para carregar devido à sobrecarga adicional introduzida pela “página inicial”. Vamos supor que o tempo de carregamento da “página de vendas” seja de 150 ms e de 350 ms para a “página de vendas”. Consequentemente, sem o lazy loading, o tempo total de carregamento seria de 450 ms.

Figura 13 — A abordagem de carregamento preguiçoso nos permite evitar solicitações desnecessárias e pesadas, buscando apenas os ativos necessários para o contexto atual.

Por outro lado, supondo o mesmo exemplo, mas buscando apenas os dados necessários, verifica-se que o tempo de resposta melhora significativamente, conforme mostrado na Figura 13. Mais precisamente, levaria apenas o tempo necessário para carregar a “página de vendas”, que é de 150 ms em vez de 450 ms.

Atualmente, o lazy loading é amplamente usado em estruturas modernas da Web, como Angular, Vue, React, Solid etc.

Desmembramento

Por exemplo, vamos imaginar um campo de pesquisa em que se deseja adiar os eventos de digitação até que o usuário digite um número mínimo de caracteres ou até mesmo acionar esses eventos após um determinado período de tempo para evitar solicitações desnecessárias. Isso se deve ao fato de que, quanto mais informações o usuário fornecer, mais eficaz será a pesquisa. Portanto, esperar um pouco é sempre bem-vindo. Resumindo, o aplicativo cliente mantém as solicitações até que tenha cumprido seus requisitos, como atender ao comprimento mínimo de entrada ou respeitar um período específico. As Figuras 14 e 15 ilustram como esse tipo de debouncing funciona.

Figura 14 — Sem o debouncing, cada evento de entrada aciona uma nova solicitação ao servidor. O envio de uma nova solicitação enquanto o usuário não tiver terminado de preencher o formulário pode resultar em um desperdício total de recursos.

Figura 15 — O debouncing restringe o número de solicitações, impedindo que elas sejam chamadas novamente até que um determinado período de tempo tenha passado.

Tomando a Figura 14 como exemplo, vamos supor que o usuário esteja procurando todos os painéis solares cujo código do fabricante comece com “SE-12”. Para realizar essa pesquisa, uma entrada é usada para recuperar os resultados com base nas palavras-chave fornecidas. Sem um debouncer, um novo evento de entrada seria gerado para cada letra, resultando no envio de uma nova solicitação ao servidor. Isso seria enviado para o servidor, o que seria um desperdício de recursos, não é mesmo? Seria muito melhor se o aplicativo pudesse aguardar alguns milissegundos antes de acionar a solicitação. Lembre-se de que os usuários são consideravelmente mais lentos em comparação com os eventos de entrada, portanto, nesse contexto, um debouncer é sempre bem-vindo.

Além do debouncing tradicional, há outras variações do mesmo conceito. O exemplo que está sendo discutido é conhecido como “modo de rastreamento”, no qual a invocação ocorre após um atraso. Há também o oposto, chamado de “leading mode”, no qual a invocação ocorre imediatamente, assim que o primeiro evento acontece. Um ótimo conteúdo sobre técnicas de debouncing pode ser encontrado em: “Explicação do debouncing e do throttling por meio de exemplos”.

Conclusão

Este breve texto demonstra técnicas e ideias para melhorar o desempenho do aplicativo, reduzindo e otimizando a comunicação cliente-servidor por meio do protocolo HTTP. Algumas delas são muito difundidas na cultura de desenvolvimento, enquanto outras nem tanto. Por exemplo, em minha opinião, a ideia de reutilizar dados locais ou valores constantes é pouco discutida pelo conteúdo técnico.

Conforme mostrado, as solicitações HTTP mal gerenciadas podem causar problemas, levando a atrasos e sobrecarga tanto para o cliente quanto para o servidor. No contexto dos requisitos de alto desempenho, isso é inaceitável.

A intenção aqui é apresentar algumas das possibilidades disponíveis e, com base em cada cenário, determinar a melhor abordagem a ser seguida. Talvez algumas das ideias mencionadas não sejam ideais devido a restrições específicas do aplicativo, portanto, o desenvolvedor precisa identificar o ponto ideal entre as restrições comerciais e o desempenho.

Independentemente da solução adotada, a principal mensagem a ser lembrada é: sempre se esforce para criar aplicativos que priorizem a eficiência; o ambiente, os usuários e suas finanças agradecem.

Referências

POSNICK, Jeff. Evite solicitações de rede desnecessárias com o cache HTTP. Web.dev, 2020. Disponível em: https://web.dev/http-cache/. Acessado em: 15 de junho de 2023.

CORBACHO, David. Explicação do debouncing e do throttling por meio de exemplos. CSS Tricks, 2016. Disponível em: https://css-tricks.com/debouncing-throttling-explained-examples/. Acessado em: 23 de junho de 2023.

--

--

Troopers Legacy
Troopers-Legacy

Experiências construídas com autonomia (de verdade), liberdade e muito amor, que vão inspirar a sua carreira.