Javascript : ce que vous ne devez pas faire avec async/await

Utiliser async/await c'est bien, sauf dans certains cas !

Javascript : ce que vous ne devez pas faire avec async/await

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.

Évidemment 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 📧
Mes formations disponibles 🎓  -5% inclus pour les lecteurs du blog

À 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 formateur/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