Au début du mois je vous partageais dans ma revue de presse ce benchmark de Muhammad A Faishal qui comparait la concaténation en string literal et la concaténation via template string en JavaScript. J'avais quelques reproches à faire sur ce benchmark, et je voulais aller un peu plus loin, donc voilà le résultat de mes tests !

Tous les graphiques peuvent être retrouvés et manipulés ici : https://kuroidoruido.github.io/bench-js-string-concat/ (les sources et les résultats bruts sont disponibles dans le dépôt github, donc n'hésitez pas à aller voir si ça vous intéresse !)

Contexte

Comme je disais j'avais quelques reproches à faire sur le benchmark. Globalement on sait qu'il a fait ses tests sur NodeJS 18.17.0, mais on ne sait pas sur quelle machine (donc le CPU et la RAM disponible) ni sur quel OS. Je trouve ça un peu dommage. De plus la comparaison s'arrête uniquement à NodeJS mais NodeJS n'est qu'une des plateformes permettant l'exécution de code JavaScript, il ne faut pas oublier Deno ou Bun qui grossissent de plus en plus, mais surtout simplement les navigateurs !

J'ai donc fait des tests sur 5 plateformes : Bun, Deno, Node, Firefox et Chromium. J'ai tout fait tourner sur la même machine, un laptop Asus Zenbook avec un CPU i7-1165G7 et 16Go de RAM, le tout sous l'OS ArchLinux (kernel 6.3.2-arch1-1 en 64bits).

À noter qu'on pourrait pousser le vice en tentant de faire le même benchmark sur un MacBook M1/M2 pour avoir une idée de ce que ça donne sur une architecture ARM de puissance similaire et en profiter pour tester sur Safari (qui n'a pas tout à fait le même moteur que Chromium). Je n'ai pas de mac en ma possession, mais si quelqu'un veut s'amuser à faire les tests, toutes les sources sont plus haut, faites le test, publiez vos résultats et je mettrai un lien vers votre benchmark avec plaisir !

Autre point qui me dérangeait un peu : on ne compare que 2 manières de faire, alors qu'on peut assembler des strings d'autres façons différentes en JavaScript : en utilisant la méthode concat ('foo'.concat('bar', 'baz') ou 'foo'.concat('bar').concat('baz')) ou en faisant une jointure de tableau (['foo', 'bar', 'baz'].join('')). On peut imaginer pas mal de combinatoire, pas mal de variantes, j'en ai retenu 7 :

  • concaténation de chaîne de caractère litérale
  • template string
  • méthode concat unique (exemple: 'foo'.concat('bar', THING, 'baz'))
  • méthode concat chaîné (exemple: 'foo'.concat('bar').concat(THING).concat('baz'))
  • méthode concat chaîné à la mode TypeScript quand on target es5 (exemple: 'foo'.concat('bar').concat(THING, 'baz'))
  • jointure de tableau (exemple: ['foo', 'bar', 'baz'].join(''))
  • jointure de tableau pré-aloué (exemple: ARRAY.join(''))

Comparaison des temps d'éxécutions

Ici j'ai pris la mesure du temps d'exécution du benchmark complet sur chaque plateforme : les 7 méthodes de concaténation avec 10 millions d'itérations chacune.

À la louche, je m'attendais à avoir une différence de performance avec 2 groupes : d'un côté les plateformes natives Bun, Deno, Node et de l'autre les navigateurs. J'ai été assez surpris de voir que ce n'est pas ça…

Comparaison du temps d'exécution sur les différentes plateformes

Quand on regarde ce graphique, on voit qu'on a pratiquement le même temps d'exécution pour Chromium, Deno et Node dont l'écart maximum entre les 3 est inférieur à 1 seconde, ce qui totalement négligeable sur une exécution de plus de 90 secondes. Bun a mis autour de 145 secondes à exécuter le benchmark complet, soit +55% de temps d'exécution, et Firefox m'a clairement déçu avec presque 180 secondes, soit presque le double du temps des bases v8.

J'ai été assez étonné du résultat sur Bun avant de me rappeler qu'il n'utilisait pas v8 sous le capot mais JavaScriptCore Engine, le moteur de Safari (donc j'ai quand même un peu de Safari dans mon benchmark contrairement à ce que je disais plus haut 😉).

Comparaison des méthodes pour chaque navigateur

On peut tirer pas mal d'observations de ces graphiques :

  • Les performances des jointures de tableau sont horribles par rapport aux autres méthodes, on peut aller jusqu'à 130 fois moins performants… et pré-alloué le tableau n'a quasiment aucun impact, donc c'est bien la jointure qui a des perfs horribles…
  • À part sur Firefox, la manière dont TypeScript génère les concat n'a aucun intérêt en termes de performance (je n'ai pas testé, mais je suppose que ce choix vise à optimiser le volume de code généré et donc la taille du bundle), l'écart restant faible entre les 3 écritures du concat à l'exception de Bun où enchaîner les concats donne des performances assez proche des literal string ou des templates strings
  • Les literals string et les templates strings sont clairement imbattables en termes de performance
  • Il faut quand même dépasser un certain volume pour voir une vraie différence de performance

Comparaison des navigateurs pour chaque méthode de concaténation

Là on prend le problème dans le sens inverse : on prend chaque méthode de concaténation et on voit la comparaison directe entre les navigateurs.

De nouveau, on peut tirer pas mal d'observations de ces graphiques :

  • Même si les performances des toutes ces plateformes sont bonnes dans l'absolue, dans ce type de tests Firefox est loin derrière pour les literal string et template string (environ 40 fois moins performant), pour ces mêmes méthodes les autres plateformes sont équivalentes, et même sur des concaténations assez conséquentes on a des performances très bonnes, on pourrait presque dire qu'on voit à peine la différence à augmenter le volume de concaténation
  • Firefox s'en sort plutôt mal face à la concurrence sur les appels enchaînés à concat (entre 2 et 3 fois moins performant), Bun s'en sort très bien pour les concat enchaîné à un seul paramètre, mais plutôt mal dès qu'on passe à 2 ou plus paramètres
  • Pour la jointure de tableau, c'est Bun qui fini par sortir du lot négativement en perdant en performance au fur et à mesure que le tableau grossi, les concurrents restants assez proches
  • Les performances des concaténations en literal string et en template string sont vraiment très proches, ce n'est pas toujours en faveur du même, c'est très partagé, en moyenne assez négligeable

Conclusion

Contrairement au benchmark que Muhammad A Faishal a réalisé, l'écart n'est pas si flagrant que ça quand on passe à l'échelle entre des literal string et des template string. J'aurai tendance du coup à préférer les template string dans la vaste majorité des cas pour leur lisibilité.

On peut aussi remarquer que l'utilisation de la méthode concat enchaîné donne des performances intéressantes aussi, en particulier sur Bun où je pense que le JIT fini par ré-écrire plus ou moins l'enchaînement de concat sous le format de litéral string concat pour gagner en performance. Évoquer ce point me permet d'évoquer le JIT justement, c'est un élément à ne pas négliger, car c'est une brique très puissante qui fonctionne très différemment en fonction des moteurs JavaScript. On voit d'ailleurs dans ces tests que les plateformes ne sont pas toutes égales entre elles, donc certains choix peuvent avoir plus de sens sur une plateforme que sur d'autres, il faut donc prendre ça aussi en compte dans nos choix.

Dans l'absolu je conclurai en disant que ces benchmarks sont intéressants mais ne doivent pas vous empêcher de prioriser la lisibilité et la maintenabilité du code. Si vous avez des problèmes de performance, oui il faut aller chercher les endroits où ça pêche pour récupérer les performances qu'on attend, mais si vous n'avez pas de problème de performance, ne cherchez pas à les régler si ça entache la lisibilité et/ou la maintenabilité !

Crédit photo : https://pixabay.com/photos/women-running-race-racing-athletes-655353/