Avant de continuer : la lecture de cet article requiert la compréhension de ce que sont les mots-clés async et await en ES6 ainsi que leur utilisation.

Si ce n'est pas le cas, je vous invite à lire ce tutoriel (en anglais) sur le site javascript.info !

Pour rappel : async permet à une fonction à retourner une promesse, tandis que le mot-clé await permet d'attendre la résolution d'une promesse de manière synchrone (ou bloquante) et de récupérer la valeur retournée directement dans une variable.

En pratique, les promesses servent à éviter le callback-hell, et async/await permettent d'éviter d'empiler les then à tout va !

Mais le piège de cette technologie réside dans le fait que du code synchrone est beaucoup plus facile à lire pour un développeur, mais il peut aussi être bien moins performant que du code asynchrone, ce qui induit parfois une mauvaise utilisation.

Exemple

Prenons une fonction qui va simuler un appel asynchrone quelconque (API, BDD, etc...) grâce à un simple timeout d'une seconde :

function t(){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      resolve();
    },1000);
  })
}

On pourra alors appeler cette fonction et attendre son retour de manière synchrone en utilisant await comme ceci :

...
console.time();
await t();
console.timeEnd();
...

La console nous indiquera que l'exécution s'est terminée en 1 seconde et quelques, ce qui correspond au temps d'exécution du script, incluant la résolution de la promesse et du blocage par await.

Jusqu'ici tout va bien.

Si vous n'êtes pas familier avec la méthode console.time(), je vous invite à lire mon article dédié aux méthodes de l'API console à connaitre.

Ce qu'il ne faut pas faire

Imaginons que nous ayons maintenant besoin de faire plusieurs appels à notre API, et d'attendre la fin de tous ces appels avant de poursuivre l'exécution de notre code.

On pourrait alors être tenté d'écrire un code comme celui-ci :

...
for(let i=0 ; i<25 ; i++){
  await t();
}
...

Et là, horreur, notre script au complet aura mis un peu plus de 25 secondes à s'exécuter ! Et oui, parce que même si notre machine (ou notre API) aurait été capable de gérer tous ces appels en parallèle, nous avons forcé notre script à attendre le retour de chaque promesse avant d'effectuer un nouvel appel.

Ce code-là, bien que facilement lisible et compréhensible par un développeur (car écrit de manière "synchrone") casse en réalité tous les avantages de Javascript qui est un langage basé sur un cycle d'évènements et asynchrone par nature !

Cette démonstration a été réalisée par un professionnel, à ne pas reproduire chez vous.

La solution

Il existe une méthode qui permet de résoudre facilement cette problématique et de garder à la fois la lisibilité du code, et les performance.

Cette méthode s'appelle Promise.all(...), elle prend en paramètre un tableau de promesses en attente de résolution et renverra elle-même une nouvelle promesse uniquement lorsque toutes les promesses contenues dans le tableau passé en paramètre seront résolues.

Voici l'équivalent de la boucle précédente dans l'exemple ci-dessous :

...
const promises = [];
for(let i=0 ; i<25 ; i++){
  promises.push(t()); //non-blocking execution
}
await Promise.all(promises);
...

Comme vous pouvez le voir, à chaque fois que l'on va faire un appel à la méthode t(), on va stocker la promesse retournée dans un tableau.

Tous les appels à la fonction vont donc pouvoir être fait en parallèle, mais le reste de notre code ne s'exécutera qu'une fois que toutes les promesses seront résolues, comme auparavant.

Résultat : l'exécution du script et des 25 appels prend 1.024s, soit 25 fois moins de temps qu'avec la solution précédente.

Evidemment dans un cas réel, le temps d'exécution final dépendra de la capacité du service asynchrone (ici remplace par le timeout) à traiter des requêtes concurrentes, mais les performances seront toujours incomparables en utilisant Promise.all !

Je vous invite à lire la documentation officielle de cette méthode sur la documentation du Mozilla Developer Network !

Par ailleurs Promise.all(...) renvoie une promesse contenant bien évidemment le tableau des données retournée par chaque promesse résolue, dans l'ordre exact des appels effectués, donc aucun problème pour récupérer toutes vos données !

Conclusion

Si vous faire des appels asynchrones dans une boucle et attendre la fin de tous les appels, utilisez Promise.all au lieu de rendre votre code complètement synchrone avec async/await !

J'espère que cet article vous aura été utile, et à bientôt sur le blog !

Les articles les plus populaires du blog

Envie de continuer à lire des articles autour du développement web (entre autres) ? Voici la sélection des articles de mon blog les plus lus par la communauté !

Voir la sélection 🚀

Recevez les articles de la semaine par e-mail pour ne rien manquer !

S'abonner à la newsletter 📧

À propos de l'auteur

Hello, je suis Nicolas Brondin-Bernard, ingénieur web indépendant depuis 2015 passionné par le partage d'expériences et de connaissances.

Aujourd'hui je suis aussi coach pour développeurs web juniors, tu peux me contacter sur nicolas@brondin.com, sur mon site ou devenir membre de ma newsletter pour ne jamais louper le meilleur article de la semaine et être tenu au courant de mes projets !


Photo par Nadine Shaabana sur Unsplash