In Galadrim, siamo abituati a lavorare con numerosi linguaggi e framework Web diversi. Sebbene ogni team e ogni sviluppatore abbiano le loro preferenze, siamo convinti che scegliere lo strumento più adatto al progetto sia la soluzione migliore.
In tal senso, siamo stati portati a realizzare numerosi backend in TypeScript e Python -i nostri stack più comuni- ma anche in Go, Rust, PHP, o ancora C#. A forza, ci siamo chiesti se esistessero vere differenze di performance tra questi linguaggi e alcuni dei loro framework rispettivi alla scala dei nostri progetti. Quando e come privilegiare una tecnologia piuttosto che un'altra?
Per rispondere a questa domanda, abbiamo realizzato un benchmark interno confrontando 4 linguaggi popolari, Go, Rust, Python e JavaScript per un progetto web tipico, un'API REST destinata a un mini social network.
Ecco il nostro feedback.
I. Grafana K6, uno strumento moderno per i test di carico
Per realizzare questo benchmark, abbiamo utilizzato Grafana K6, uno strumento open-source, scritto in Go, e progettato da Grafana Labs che va a fare concorrenza a strumenti come JMeter, costruito sulla JVM (Java).
Ciò che cambia le carte in tavola
Semplice da usare : Progettato per essere usato in CLI, K6 è molto semplice da usare e gli scenari di test sono semplici script JavaScript (o TypeScript). Qualsiasi sviluppatore del team può leggere, scrivere e mantenere i test senza formazione specifica.
Performance ed efficienza : Essendo scritto in Go, K6 consuma pochissima memoria ed è molto performante. Una singola macchina standard può quindi simulare migliaia di utenti virtuali laddove altri strumenti richiederebbero un intero cluster.
Integrazione CI/CD : Essendo uno strumento CLI scriptabile, si integra nativamente nelle tue pipeline (GitHub Actions, GitLab CI, ecc.) per automatizzare i test di non-regressione delle performance.
Come funziona?
K6 orchestra dei VUs (Virtual Users). Ogni VU agisce come un utente che obbedisce a uno script predefinito ripetuto indefinitamente :
Esegue richieste HTTP (o WebSocket, gRPC).
Verifica le risposte (status 200, contenuto del JSON...).
Rispetta dei tempi di pausa (sleep) per simulare un comportamento umano realistico.
L'outil agrège ensuite les résultats en temps réel et fournit des métriques bien plus pertinentes qu'une simple "moyenne" pour comprendre la latence ressentie par les utilisateurs. Une métrique intéressante est le 95ème centile ou p95 qui correspond à la valeur telle que 95% des latences observées sont en dessous et 5% sont au-dessus.
II. Il protocollo di test: un mini social network
Pour que les résultats soient pertinents, nous avons simulé un cas d'usage représentatif des projets que nous développons pour nos clients : un mini réseau social. Ainsi, nos backends de test doivent implémenter les fonctionnalités suivantes :
Authentification : Login, récupération du profil courant.
Gestion des utilisateurs : Création, listing, modification de profil.
Gestion des posts : Publication, lecture de flux, suppression.
Fonctionnalités d'un réseau social : Commentaires, likes / unlikes.
Environnement technique
Nous voulions également limiter au maximum l'utilisation d'outils propres à chaque écosystème pour une raison double : ne pas alourdir les tests avec des dépendances supplémentaires et éviter d'introduire des biais lors de la comparaison des performances.
Dans ce but, nous avons décidé de partir d'un simple fichier OpenAPI décrivant l'API puis d'implémenter de façon minimaliste les routes dans chaque langage sans utiliser d'ORM ou d'outil de mapping JSON. Nous avons écrit nos requêtes de manipulation de la base de données directement en SQL, afin que chaque backend exécute exactement la même requête avec la même base, de la même façon.
Les tests ont été réalisés sur un MacBook Pro M3 (36 GB RAM) avec des conteneurs Docker limités à 4 CPUs et 8 GB de RAM pour chaque backend. Les valeurs absolues de requêtes par seconde (RPS) mentionnées ci-après ne sont pas à prendre telles quelles mais plutôt à comparer entre elles car dépendantes de votre configuration.
Le scénario de test de chaque VU
Notre workflow de test ne se contente pas de spammer une route, il simule le parcours complet d'un utilisateur :
Login administrateur et création d'un nouvel utilisateur.
Login du nouvel utilisateur.
Publication d'un post.
Lecture du feed.
Ajout de commentaire et like sur un post.
Suppression des données (nettoyage).
Nous avons fait varier la charge de 50 VUs à 1000 VUs simultanés pour voir quand les backends craquent.
Ci-dessous un exemple d'exécution pour Express (Bun, 1000 VUs).
III. Gli insegnamenti
Comparaison RPS sur tous les scénarios
Temps de réponse P95 par scénario
Performance brute : le duel Go vs Rust
Si vous cherchez la performance pure pour des systèmes à très fort trafic, le match se joue entre Go e Rust. Sur l'échelle de nos tests, il n'y a pas de gagnant clair et les deux langages atteignent des performances similaires (> 20 000 RPS). Ces langages brillent également par leur résilience sous forte charge avec des p95s autour de 85ms, bien en-dessous des 200ms acceptées dans le cadre de ce test.
Go (avec Fiber) : Sa simplicité et sa gestion des goroutines en font un monstre d'efficacité pour les API web.
Rust (avec Axum) : Avec de très solides performances, Rust offre également des garanties de sécurité mémoire et de stabilité inégalées malgré une prise en main plus complexe.
Verdict : Si vous avez besoin de performance brute, Go et Rust sont des choix solides. Tandis que le premier met l'accent sur l'expérience de développement, le second brille dans la confection de systèmes robustes.
Rapidité de développement : Python vs JavaScript (Node/Bun)
Pour la majorité des projets (MVP, startups, outils internes), la vitesse de développement prime souvent sur la performance brute. En effet, le langage de programmation choisi est rarement le facteur limitant pour la performance d'un backend en comparaison avec l'optimisation de l'architecture, de la base de données, et des entrées / sorties réseaux ou systèmes.
Python (FastAPI) : Facile à prendre en main, avec une syntaxe concise et claire, excellent pour la data, il montre des limites en termes de performance sous forte charge. Par ailleurs, Python a rapidement un impact important sur la mémoire utilisée côté serveur et ne conviendra donc pas forcément pour des applications à fortes contraintes hardware (p.ex. applications embarquées). Dans nos tests, il tient bien la charge "moyenne" mais commence à échouer sous une charge lourde (500+ utilisateurs simultanés) avec un p95 de 420ms pour 1 000 VUs. C'est un excellent compromis entre la productivité et la performance, tant que le trafic reste raisonnable (~7 000 RPS).
JavaScript (Node vs Bun) : Node.js est le runtime JavaScript le plus populaire, et il est basé sur le moteur V8 de Google. Bun est un runtime JavaScript plus récent, et il est construit sur le moteur JavaScript d'Apple. JavaScript (mais aussi TypeScript) est un langage de programmation qui permet de développer des applications web. Il a l'énorme avantage de pouvoir être exécuté côté client et côté serveur, ce qui permet de développer des applications web full-stack avec un seul langage. Cela implique la possibilité de partager du code commun entre le client et le serveur (business logic, validation de données, types, etc.), mais aussi de n'utiliser et ne maîtriser qu'un seul langage pour le développement de l'application. Avec environ 10 000 RPS atteintes dans les deux cas, l'écosystème JavaScript propose de solides performances tout en conservant un p95 de ~170ms dans le pire scénario.
Verdict : Si vous avez une équipe plutôt full-stack, JavaScript/TypeScript est une option très solide pour obtenir d'excellentes performances sans changer de langage. Pour de l'utilisation purement backend ou de la Data Science avec une itération rapide Python est une très bonne alternative mais demandera une architecture plus robuste (caching, scaling horizontal) et plus d'attention à l'optimisation du code pour atteindre des performances comparables.