Startseite / Artikel / TypeScript ohne Node oder V8: Vergleich der nativen ScriptC-Binärdateien

TypeScript ohne Node oder V8: Vergleich der nativen ScriptC-Binärdateien

Kaltstart, RSS, Durchsatz sowie Kompromisse bei der CPU-Nutzung, wenn derselbe TypeScript-HTTP-Server als natives Scriptc-Binärprogramm statt mit Node.js und V8 ausgeführt wird.

975 Wörter

Benchmark-Ergebnisse aus dem Betrieb eines TypeScript-HTTP-Dienstes als natives Binärdatei anstelle unter Node.js mit V8.

JavaScript für den Backend-Bereich benötigt fast immer einen Host-Runtime. Bei Node, Deno oder Bun ist dieser in der Regel Googles V8: JIT-Kompilierung in Maschinencode, Garbage Collection sowie ein für Konkurrenz ausgelegter Event-Loop.

In einem alternativen Experiment wird untersucht, ob TypeScript-Server auf Node, V8 sowie alle anderen JS-Engines verzichten und stattdessen in eine eigenständige, selbstständige native Binärdatei kompiliert werden können.

Der experimentelle Compiler scriptc von Vercel Labs verfolgt diesen Ansatz. TypeScript wird zunächst in LLVM IR oder intermediäres C umgewandelt und anschließend mit herkömmlichen Compilern wie clang abgeschlossen.

Derselbe TypeScript-HTTP-Dienst wurde in zwei Konfigurationen gemessen:

  1. Node.js v22.21.1, der mit integrierter Typenentfernung ausgeführt wird
  • scriptc v0.0.32 erzeugt ein natives ELF x86-64-Binärdatei
  • Die untenstehenden Ergebnisse fassen zusammen, was sich nach dem Entfernen von V8 von diesem Server geändert hat.

    1. Verglichene Arbeitslasten

    Um Fairness zu gewährleisten, verwendet der Server das Standard-http-Modul von Node ohne zusätzliche Abhängigkeiten. Quelltext, Geschäftslogik sowie HTTP-Grenzen bleiben auf beiden Hosts identisch.

    Betroffene Routen:

    • /health – günstige Bereitschaftsprüfung
    • /json – strukturierte JSON-Daten
    • /compute – intensive numerische Schleife
    • /hash – SHA-256 über 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. Latenz beim ersten Starten

    Serverless-Plattformen, Edge-Funktionen sowie Container, die auf Null skaliert werden, kümmern sich nicht um die Startzeit des Prozesses. Die Messung erstreckte sich vom Erstellen des Prozesses bis zur erfolgreichen Antwort auf /health und wurde zehnmal wiederholt.

    • Node.js lag im Durchschnitt bei etwa 232,16 ms
    • scriptc lag im Durchschnitt bei etwa 14,23 ms (etwa 16,3× schneller)

    Durch das Auslassen des V8-Startvorgangs, der Parsing-Kosten sowie der Einrichtung zur Entfernung von Typinformationen kann das native Binärdatei fast sofort antworten.

    3. Inaktiver RSS und virtuelle Größe

    V8 reserviert im Voraus Heap- und JIT-Puffer. Wie viel Speicher wird während des Wartens des Prozesses reserviert?

    • Der physische RSS für Node lag bei etwa 70,40 MB
    • Der physische RSS für scriptc lag bei etwa 2,55 MB (etwa 27,6× sparsamer)

    Die virtuelle Größe zeigte ein deutlicheres Bild: Node wies aufgrund der Kartenstrategie von V8 etwa 21,5 GB VSZ auf, während scriptc bei etwa 4,09 MB blieb.

    4. Anfragenrate und Latenz

    oha erzeugte eine Last über 10 Sekunden bei 10, 100 und 500 gleichzeitigen Clients.

    Lichtprobe (/health)

    • Bei 500 gleichzeitigen Clients erreichte scriptc etwa 29.828 RPS, während Node nur 9.966 RPS erreichte (~2,99×).
    • Bei 100 gleichzeitigen Clients lag die p50-Latenz bei scriptc bei etwa 2,78 ms, während sie bei Node 8,19 ms betrug.

    JSON-Antworten (/json)

    Einschließlich Serialisierung erreichte scriptc bei 100 gleichzeitigen Clients etwa 24.209 RPS, während Node nur 10.012 RPS erreichte (~2,42×).

    5. CPU-Pfade: Vorab-Kompilierung versus Just-in-Time

    CPU-orientierte Routen zeigen auf, an welchen Stellen sich AOT und JIT unterscheiden.

    Aritmetische Schleife auf /compute

    Der Handler wiederholt (result + i * 31) % 1_000_000_007.

    • Node erreicht etwa 2.835 RPS (p50 bei ca. 27,38 ms)
    • scriptc erreicht etwa 2.620 RPS (p50 bei ca. 32,21 ms)

    Hier gewann Node’s JIT um etwa 15%. Die von scriptc erzeugte C-Code-Analyse zeigt lokale Variablen als C double-Werte sowie die Modulo-Operation über fmod():

    double sc_t11 = fmod(sc_t9, sc_t10);
    

    Der Aufruf von fmod() verursacht Kosten durch die dynamische Bibliothek. V8 beobachtet zur Laufzeit ein verhalten, das dem von Ganzzahlen ähnelt, und kann dies auf eine kostengünstigere native 32-Bit-Ganzzahldivision spezialisieren.

    SHA-256 auf /hash

    Das Hashing erfolgt über node:crypto.

    • Node: etwa 9.873 RPS (p50 bei ca. 8,45 ms)
    • scriptc: etwa 28.849 RPS (p50 bei ca. 2,97 ms)

    scriptc ist etwa 2,9× schneller. Node wechselt zweimal zum C++ für .update() und .digest() und zahlt jedes Mal die Overhead-Kosten der OpenSSL-Bindung. scriptc kürzt die Abfolge auf einen einzigen nativen Aufruf ab:

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

    Durch den Betrieb direkt im nativen Speicher entfällt die Hin- und Rückreise zwischen JS und dem nativen Code.

    6. Größe der Container-Image

    Paketierung für Kubernetes-basierte Bereitstellungen:

    • Bilder basierend auf node:22-slim haben eine Größe von etwa 120 MB
    • Bilder basierend auf debian:bookworm-slim, die nur das dynamisch gelinkte scriptc-Binärdatei enthalten, haben eine Größe von etwa 80 MB; statische oder scratch-basierte Paketierungen können bis zu 25 MB erreichen

    7. Die aktuellen Grenzen von scriptc

    Der Compiler bleibt experimentell und eingeschränkt:

    Dynamische Muster: Der intensive Einsatz von any oder hochgradig dynamischem JavaScript zwingt zum Einsatz von QuickJS (~620KB), wodurch die Vorteile der AOT-Kompilierung verloren gehen.

    Paket-Ökosystem: Viele npm-Module sind auf Node-Internals oder Reflexion angewiesen, die nicht statisch kompiliert werden können.

    Fehlender JIT: Wie /compute zeigte, ist eine laufzeitbasierte Typspezialisierung durch einen JIT nicht verfügbar.

    8. Praktische Schlussfolgerung

    Bleiben Sie bei Node, wenn:

    • große npm-Abhängigkeitsgraphen unerlässlich sind
    • der Code dynamisch oder locker typisiert ist
    • ein JIT bei numerischen Schleifen mit dynamischem Typ hilft

    Berücksichtigen Sie scriptc, wenn:

    • Skalierbare Hosts benötigen Startzeiten unter 15 ms sowie kalte RSS-Werte unter 3 MB
    • Dienste umfassen in der Regel native Bibliotheken (Verschlüsselung, Netzwerk) als schlanke API

    Reproduktionsprobleme finden sich in diesem GitHub-Repository.