Accueil / Articles / TypeScript sans Node ni V8 : Évaluation des binaires natifs de scriptc

TypeScript sans Node ni V8 : Évaluation des binaires natifs de scriptc

Démarrage en froid, RSS, débit et compromis en termes de CPU lorsque le même serveur HTTP TypeScript est exécuté sous forme de binaire natif ScriptC plutôt qu’avec Node.js et V8.

975 mots

Notes de benchmark issues en exécutant un service HTTP TypeScript sous forme de binaire natif plutôt que via Node.js avec V8.

Le JavaScript backend a presque toujours besoin d’un environnement d’exécution hôte. Sur Node, Deno ou Bun, cet hôte est généralement V8 de Google : compilation JIT en code machine, collecte de déchets et boucle d’événements optimisée pour la concurrence.

Une expérience alternative cherche à savoir si les serveurs TypeScript peuvent se passer de Node, V8 et de tout moteur JS, en se compilant plutôt en un binaire natif autonome.

Le compilateur expérimental de Vercel Labs, scriptc, suit cette approche. Le TypeScript est d’abord converti en IR LLVM ou en C intermédiaire, puis compilé à l’aide de compilateurs classiques tels que clang.

Le même service HTTP TypeScript a été mesuré dans deux configurations :

  1. Node.js v22.21.1 en exécution avec suppression intégrée des types
  • scriptc v0.0.32 générant un binaire ELF natif x86-64
  • Les résultats ci-dessous résument les changements survenus après la suppression de V8 de ce serveur.

    1. Charge de travail comparée

    La fiabilité était importante, c’est pourquoi le serveur utilise le module http standard de Node sans dépendances supplémentaires. Le texte source, la logique métier et les limites HTTP restent identiques sur les deux hôtes.

    Routes prises en charge :

    • /health — sondage de disponibilité peu coûteux
    • /json — corps JSON structuré
    • /compute — boucle numérique intensive
    • /hash — SHA-256 via node:crypto
    import { createServer } from "node:http";
    import { createHash } from "node:crypto";
    
    const server = createServer((req, res) => {
      const url = new URL(req.url ?? "/", "http://localhost");
    
      if (req.method === "GET" && url.pathname === "/health") {
        res.writeHead(200, { "content-type": "application/json" });
        return res.end(JSON.stringify({ status: "ok" }));
      }
    
      // Additional routes (/json, /compute, /hash)
    });
    
    server.listen(process.env.PORT || 3000);
    

    2. Latence de démarrage

    Les plateformes serverless, les fonctions edge et les conteneurs qui peuvent être réduits à zéro ne se soucient pas du temps de démarrage des processus. Le chronométrage a été effectué depuis la création du processus jusqu’à ce que /health réponde avec succès, et cela a été reproduit dix fois.

    • Node.js a affiché en moyenne environ 232,16 ms
    • scriptc a affiché en moyenne environ 14,23 ms (environ 16,3 fois plus rapide)

    L’évitement du démarrage de V8, des coûts de parsing et des préparatifs liés au suppression des types permet au binaire natif de répondre presque immédiatement.

    3. RSS inactif et taille virtuelle

    V8 réserve à l’avance la mémoire heap et les buffers JIT. Quelle quantité de mémoire est réservée pendant que le processus attend ?

    • Le RSS physique pour Node était d’environ 70,40 MB
    • Le RSS physique pour scriptc était d’environ 2,55 MB (environ 27,6 fois moins gourmand)

    La taille virtuelle révélait une différence plus marquée : Node affichait environ 21,5 GB de VSZ grâce à la stratégie de mappage de V8, tandis que scriptc restait autour de 4,09 MB.

    4. Débit de requêtes et latence

    oha a généré des charges pendant 10 secondes avec 10, 100 et 500 clients simultanés.

    Probe légère (/health)

    • Avec 500 clients simultanés, scriptc a atteint environ 29 828 RPS, contre 9 966 RPS pour Node (~2,99×).
    • Avec 100 clients simultanés, la latence p50 était d’environ 2,78 ms pour scriptc, contre 8,19 ms pour Node.

    Réponses JSON (/json)

    Avec la sérialisation incluse, scriptc a atteint environ 24 209 RPS, contre 10 012 RPS pour Node avec 100 clients simultanés (~2,42×).

    5. Chemins CPU : en amont versus juste à temps

    Les itinéraires orientés CPU montrent où AOT et JIT diffèrent.

    Boucle arithmétique sur /compute

    Le gestionnaire répète l’opération (result + i * 31) % 1_000_000_007.

    • Node gère environ 2 835 RPS (p50 autour de 27,38 ms)
    • scriptc gère environ 2 620 RPS (p50 autour de 32,21 ms)

    Ici, le JIT de Node l’emporte d’environ 15 %. En examinant le code C généré par scriptc, on constate que les variables locales sont représentées sous forme de valeurs double en C et que l’opération modulo est effectuée via fmod():

    double sc_t11 = fmod(sc_t9, sc_t10);
    

    L’appel à fmod() entraîne des coûts liés aux bibliothèques dynamiques. V8 observe un comportement similaire à celui des entiers en temps de exécution et peut se spécialiser pour utiliser une division entière native à 32 bits, ce qui est moins coûteux.

    SHA-256 sur /hash

    L’opération de hachage passe par node:crypto.

    • Node : environ 9 873 RPS (p50 autour de 8,45 ms)
    • scriptc : environ 28 849 RPS (p50 autour de 2,97 ms)

    scriptc est environ 2,9× plus rapide. Node passe deux fois au C++ pour les fonctions .update() et .digest(), ce qui entraîne des coûts supplémentaires liés à l’interface OpenSSL à chaque appel. scriptc, quant à lui, regroupe tout cela en une seule appelation native :

    ScrStr *sc_t212 = scr_crypto_hash_digest_str(sc_t209, sc_t210, sc_t211);
    

    Le fait d’exécuter directement en mémoire native élimine ce va-et-vient entre JavaScript et code natif.

    6. Taille de l’image du conteneur

    Emballage pour des déploiements de type Kubernetes :

    • Les images basées sur node:22-slim atteignent environ 120 MB
    • Les images basées sur debian:bookworm-slim, ne contenant que le binaire scriptc en liaison dynamique, atteignent environ 80 MB ; un emballage statique ou de type scratch peut atteindre jusqu’à 25 MB

    7. Limites importantes de scriptc aujourd’hui

    Le compilateur reste expérimental et restreint :

    Modèles dynamiques : une utilisation intensive de any ou de JavaScript hautement dynamique oblige à l’intégration de QuickJS (~620 KB), ce qui fait perdre les avantages en termes de vitesse AOT.

    Écosystème des paquets : de nombreux modules npm dépendent des fonctionnalités internes de Node ou de mécanismes de réflexion qui ne peuvent pas être compilés statiquement.

    Absence de JIT : comme l’a montré /compute, la spécialisation des types en temps de exécution grâce à un JIT n’est pas disponible.

    8. Conclusion pratique

    Restez sur Node lorsque :

    • Des graphes de dépendances npm importants sont indispensables
    • Le code est orienté dynamique ou à typage lâche
    • L’aide d’un JIT pour les boucles numériques à typage dynamique est cruciale

    Envisagez scriptc lorsque :

    • Les hôtes à échelle zéro ou aux limites nécessitent des début de traitement en moins de 15 ms ainsi que des ressources RSS froides inférieures à 3 MB
    • Le service encapsule principalement des bibliothèques natives (chiffrement, réseaux) sous la forme d’une API légère

    Les artefacts de reproduction se trouvent dans ce répertoire GitHub.