DEV Community

Ronaldo Modesto
Ronaldo Modesto

Posted on

Otimizando a chamada de múltiplas promises [ Javascript ]

Fala povo bonito 😀
Hoje estou passando aqui para trazer uma dica rápida sobre promises que muita gente ainda desconhece,principalmente aqueles que estão conhecendo o ambiente Javascript/NodeJs, sobre como otimizar a execução de várias chamadas assíncronas (Promises) no javascript. Bora conferir essa dica 🙃?

async/await dentro de um lopp = bad news

Para começar vamos definir de forma bem sucinta o que é uma Promise.
Promises são ações que irão se completar em algum ponto futuro no tempo. Ou seja, são funções cujo valor você não conhece de imediato e ele só estará disponível após um intervalo x de tempo. No javascript uma função é caracterizada como promise quando possui o modificador async antes do nome. Uma promise pode ser resolvida( quando ocorre tudo certo na chamada da função) ou rejeitada ( quando ocorre algum erro na chamada da função ).
Segue uma definição mais detalhada do que é uma promise: clique aqui

No javascript, na maioria das vezes em que um dev precisa executar várias chamadas assíncronas sobre um array de valores ele vai, fatalmente, executar um for loop, um for..in ou mesmo um for..of para iterar em cada valor do array, esperar a promise terminar, fazer o que tiver que fazer para só então passar para a próxima chamada. Traduzindo em código, imagine que temos uma função que faz o envio de um e-mail e que essa função é assíncrona (ou seja, gera uma promise quando chamada), fica algo mais ou menos asssim:

const sendMail = async (email) => { // lógica que envia um e-mail para o email informado como parâmetro };
const emails = [a, b, c, ...];
for (let i = 0; i < valores.length ; i++){
    await sendMail(emails[i]);
}
/// resto do seu código ...
Enter fullscreen mode Exit fullscreen mode

Essa abordagem não está de todo errada, afinal de contas, toda abordagem que produz o resultado esperado é por definição uma abordagem certa. Porém ela possui um gravíssimo problema de performance. Vamos analisar rapidamente o código e ver o que está acontecendo.

Dentro do nosso loop estamos esperando a promise ser resolvida, ou seja, a cada iteração do loop o nosso código pára e fica ali esperando o retorno da chamada para a função sendmail que fizemos. Se cada chamada dessa função levar 3 segundos e tivermos um array com 5 emails vamos levar 15 segundos para enviar todos, agora imagina que temos um array com uma feedList de 1 milhão de emails para enviar... Só de curiosidade vou dizer quanto tempo vai levar, 833.3333333 HORAS!!!. Impraticável.
Além disso, essa abordagem esconde um problema pior ainda, que é o bloqueio do Event Loop, esse erro tem potencial para fazer todo o seu servidor parar. Eu explico mais detalhadamente o que é o event loop e porque devemos nos preocupar com essa característica no nodeJs, caso queira conferir aqui vai o link: clique aqui para acesssar o artigo

Melhorando o cenário

Ok ok, já sabemos que usar async/await dentro de um loop é uma má ideia. Porém nós ainda temos os nossos 1 milhão de email para enviar, como vamos fazer ?
É aí que entra uma funcionalidade salvadora para nós. E ela se chama Promise.all !!

Segue a definição do site da mozila sobre esse método (traduzido):
O método Promise.all() recebe um iterável de promessas como entrada e retorna uma única Promessa que resolve para uma matriz dos resultados das promessas de entrada. Essa promessa retornada será cumprida quando todas as promessas de entrada forem cumpridas ou se o iterável de entrada não contiver promessas. Ele rejeita imediatamente após qualquer uma das promessas de entrada rejeitando ou não promessas lançando um erro, e irá rejeitar com esta primeira mensagem/erro de rejeição.

Traduzindo, o método Promise.all recebe um array de promises e retorna uma única promise que só irá ser resolvida quando todas as promises do array que foi passado forem resolvidas.
Mas ai você deve estar pensando, "Poxa grande porcaria, vai demorar do mesmo jeito 🤷‍♂️". Calma jovem padawan, eu explico.
Acontece que esse método não irá esperar uma promise se resolver para executar a próxima, ele vai executá-las concorrentemente, resolvendo para um array de resultados quando todas as promises se completarem, ou, alguma delas incorrer em erro.
Dessa forma, se você tiver um array com três promises que levam, respectivamente 2,4 e 5 segundos para executar, a sua chamada usando Promise.all vai levar no máximo 5 segundos, já com a abordagem utilizando async/await dentro de um loop, levaríamos 2 + 4 + 5 = 11 segundos.

Promise.all é realmente uma mão na roda 😀

Traduzindo em imagens as duas abordagens, temos:

gráfico promises resolvidas de forma sequencial

gráfico promises resolvidas de forma concorrente
fonte: própria autoria

Teste prático

Ok ok Ronaldo, muito bonito na teoria, mas me mostra o código!!!
Para os mais céticos, fiz um pequeno código de teste que ilustra a situação acima descrita, ou seja, o envio massivo de e-mails.
Neste exemplo vamos utilizar um array com 25 emails, considerando que cada chamada da nossa função sendMail leva 3 segundos ( Eita provedor de e-mail lento, Jesus Cristo).
Esse é o nosso código, você pode testá-lo online. Sugiro que você mude os valores e compile o código para entender e sentir a diferença entre as abordagens
Para editar e executar o código, clique aqui

 const timeout = 3000; // tempo que as promises levarão para se resolver

 const userMails = ['teste1@gmail.com','teste2@gmail.com','teste3@gmail.com','teste4@gmail.com','teste5@gmail.com',
 'teste6@gmail.com','teste7@gmail.com','teste8@gmail.com','teste9@gmail.com','teste10@gmail.com',
 'teste11@gmail.com','teste12@gmail.com','teste13@gmail.com','teste14@gmail.com','teste15@gmail.com',
 'teste16@gmail.com','teste17@gmail.com','teste18@gmail.com','teste19@gmail.com','teste20@gmail.com',
'teste21@gmail.com','teste22@gmail.com','teste23@gmail.com','teste24@gmail.com','teste25@gmail.com'
 ]

const sendMail = (email) => {

  return new Promise((resolve, reject) => {
    console.log(`Enviando email para ${email}`);

    setTimeout(() => {
      console.log(`Email enviado para ${email}`);

      resolve(`ok`);
    }, timeout);
  });
};

async function enviandoEmailsComForLoop() {

  const start = Date.now();

  for (let i = 0; i < userMails.length; i++) {
    await sendMail(userMails[i]);
  }
  const end = Date.now();

  console.log("Tempo total gasto para enviar com loop for:  ", end - start);
}

async function enviandoEmailsComPromiseAll() {
  const start = Date.now();

  await Promise.all(userMails.map((email) => sendMail(email)));

  const end = Date.now();

  console.log("Tempo total gasto para enviar com promise.all:  ", end - start);
}

enviandoEmailsComForLoop();

enviandoEmailsComPromiseAll();
Enter fullscreen mode Exit fullscreen mode

Vamos comparar os resultados em ambas as abordagens.

Com o async/wait dentro de um loop temos o seguinte resultado:

Enviando email para teste1@gmail.com
Email enviado para teste1@gmail.com
Enviando email para teste2@gmail.com
Email enviado para teste2@gmail.com
Enviando email para teste3@gmail.com
Email enviado para teste3@gmail.com
...(resumindo)
Email enviado para teste25@gmail.com

Tempo total gasto para enviar com loop for:  75.228 segundos
Enter fullscreen mode Exit fullscreen mode

Ou seja, gastou 75.228 segundos, pouco mais de 1 minuto

Utilizando o promise.all temos essa saída:

Enviando email para teste1@gmail.com
Enviando email para teste1@gmail.com
... (resumindo)
Enviando email para teste25@gmail.com

 Tempo total gasto para enviar com promise.all:  3.018 segundos
Enter fullscreen mode Exit fullscreen mode

Ou seja, levou 3.018 segundos para enviar os 25 e-mails. Bem mais rápido 😀

Nem tudo são flores...

Como aprendemos desde o início, nem tudo são flores e especialmente em computação não existe almoço de graça. Apesar de trazer um grande benefício, o uso desse método possui alguns contras. A saber:

  • Se qualquer uma das promises for rejeitada, a promise intera do método Pormise.all será rejeitada ( Veremos outro método que nos auxilia neste caso )
  • Nem todos os problemas podem ser resolvidos utilizando este método, por exemplo se você depende do resultado da promise 1 para chamar a promise 2 então não será possível utilizar esse método dado que ele resolve todas as promises para um array de resultados, não permitindo operar promises individualmente.
  • Ele retorna um array de results, o que pode ser mais chato de fazer o parse dependendo dos possíveis resultados das promises chamadas
  • Naturalmente ele vai consumir um pouquinho a mais de recursos para efetuar essas operações de forma concorrente.

Algumas observações

1) Existe ainda o método Promise.allSettled() que funciona exatamente como o método Promise.all() porém com a vantagem de que, mesmo que qualquer uma das promises falhe ele vai continuar a execução das outras.

2) Existe ainda o método Promise.any que funciona exatamente como os métodos Promise.all() e Promise.allSettled(), porém diferentemente dos outros dois métodos, ele não irá esperar todas as promises se resolverem, tão logo qualquer uma das promises retorne algo, o método irá retornar o valor retornado pela promise que terminou primeiro.

3) 🛑Importante🛑!!! Não confunda concorrente com paralelo. O método Promise.all trabalha de forma concorrente. Ou seja, ele vai disparar as chamadas para o sistema interno do nodeJs e então a cada loop do EventLoop o V8(motor por debaixo do nodeJs) vai checando cada uma das promises para ver se está resolvida. Mas tenha em mente que isso é concorrência e não paralelismo. Paralelismo seria se todas elas fossem executadas uma em cada thread/Core no sistema, o que não é o caso aqui.

Uma dica importante

Para evitar esse tipo de problema é aconselhável o uso de linters de código, hoje eles já possuem muitas regras mapeadas que impedem que esse tipo de situação aconteça, detectando e alertando o desenvolvedor assim que o código é escrito.
Essa situação que analisamos, por exemplo, está descrita em uma regra do EsLint, caso queira conferir, essa é a regra Eslint Rule

Caso você deseje saber mais sobre os outros métodos disponíveis, segue a documentação oficial da Mozila clique aqui

Bom é isso pessoal, essa foi a dica de hoje espero que esse conhecimento lhes seja útil.
Fiquem com Deus e até a próxima 🙂

Top comments (2)

Collapse
 
urielsouza29 profile image
Uriel dos Santos Souza

Quanto tempo levaria para mandar 1 milhão de emails com promise?

Collapse
 
r9n profile image
Ronaldo Modesto

Opa, boa pergunta amigo. Então, depende muito. Neste caso que analisei aí em cima eu desconsiderei muuitas coisas kkkkk, por exemplo qual a configuração da máquina onde esses envios seriam feitos, rede, qual o provedor de e-mails utilizados, qual o tipo de e-mail ( imagina enviar um e-mail com 4 anexos de 4mb cada). São muitos fatores que podem influenciar nesse funcionamento, é praticamente impossível dizer um valor de tempo, mesmo aproximado. O que posso é que sempre podemos tentar otimizar as operações conforme o tipo de problema e nesse caso que analisamos o promise.all ajuda e bastante 🙂