Strona główna / Artykuły / TypeScript bez Node lub V8: Porównanie wydajności natywnych binarów scriptc

TypeScript bez Node lub V8: Porównanie wydajności natywnych binarów scriptc

Kwestie związane z rozruchem na zimno, RSS, przepustowością oraz wydajnością CPU, gdy ten sam serwer HTTP typu TypeScript jest uruchamiany jako binarnik natywny ScriptC zamiast w środowisku Node.js z silnikiem V8.

975 słów

Uwagi z testów porównawczych dotyczących uruchamiania usługi HTTP w TypeScript jako binarza natywnego zamiast pod Node.js z V8.

Język JavaScript używany w warstwie backendowej prawie zawsze wymaga środowiska wykonawczego. W Node, Deno lub Bun tym środowiskiem jest zazwyczaj V8 od Google: kompilacja JIT do kodu maszynowego, zbieranie śmieci oraz pętla zdarzeń dostosowana do równoległości.

Inne eksperymenty sprawdzają, czy serwery w TypeScript mogą pominąć Node, V8 oraz wszystkie inne silniki JS, kompilując kod zamiast tego do jednego samodzielnie zawartego binarza natywnego.

Eksperymentalny kompilator z Vercel Labs, scriptc, podąża tą drogą. TypeScript jest najpierw przekształcany na LLVM IR lub pośredni język C, a następnie kompilowany za pomocą zwykłych kompilatorów, takich jak clang.

Tę samą usługę HTTP w TypeScript sprawdzono w dwóch konfiguracjach:

  1. Node.js v22.21.1 uruchamiany z wbudowanym usuwaniem informacji typów
  • scriptc v0.0.32 tworzący natywny plik binarny ELF x86-64
  • Poniższe wyniki podsumowują zmiany po usunięciu V8 z tego serwera.

    1. Obciążenie do porównania

    Brała pod uwagę się sprawiedliwość, dlatego serwer używa standardowego modułu http w Node bez dodatkowych zależności. Tekst źródłowy, logika biznesowa oraz granice HTTP pozostają identyczne na obu hostach.

    Obsługiwane trasy:

    • /health — tania próba sprawdzenia gotowości
    • /json — ustrukturyzowany ciało wiadomości w formacie JSON
    • /compute — intensywny pętla liczbowy
    • /hash — SHA-256 za pomocą 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. Opóźnienie przy pierwszym uruchomieniu

    Platformy serverless, funkcje edge oraz kontenery, które mogą zostać skalowane do poziomu zera, nie przejmują się czasem uruchamiania procesu. Mierzono czas od utworzenia procesu do pomyślnego odpowiedzi od /health, przy czym pomiar powtarzano dziesięć razy.

    • Node.js osiągnął średnio około 232,16 ms
    • scriptc osiągnął średnio około 14,23 ms (około 16,3× szybciej)

    Pominięcie fazy uruchamiania V8, kosztów analizy kodu oraz konfiguracji usuwania informacji typowych pozwala binarnemu plikowi odpowiedzieć niemal natychmiast.

    3. Zużycie pamięci RSS i rozmiar wirtualny

    V8 z góry rezerwuje pamięć heap oraz bufory JIT. Ile pamięci pozostaje zarezerwowane, podczas gdy proces czeka?

    • Faktyczne zużycie pamięci RSS w Node wynosiło około 70,40 MB
    • Faktyczne zużycie pamięci RSS w scriptc wynosiło około 2,55 MB (około 27,6× mniej pamięci)

    Rzeczywisty rozmiar pokazał wyraźniejszy obraz: Node wykazywał około 21,5 GB VSZ dzięki strategii mapowania V8, podczas gdy scriptc utrzymywał się na poziomie około 4,09 MB.

    4. Przepustowość żądań i opóźnienie

    oha generował obciążenie przez 10 sekund przy 10, 100 i 500 jednoczesnych klientach.

    Lekka próba (/health)

    • Przy 500 jednoczesnych klientach scriptc osiągnął około 29 828 RPS, podczas gdy Node osiągnął 9 966 RPS (~2,99×).
    • Przy 100 jednoczesnych klientach opóźnienie p50 wynosiło około 2,78 ms dla scriptc w porównaniu z 8,19 ms dla Node.

    Odpowiedzi w formacie JSON (/json)

    Z uwzględnieniem serializacji scriptc osiągnął około 24 209 RPS, podczas gdy Node osiągnął 10 012 RPS przy 100 jednoczesnych klientach (~2,42×).

    5. Ścieżki CPU: ahead-of-time versus just-in-time

    Ścieżki skierowane na CPU pokazują, gdzie różnią się podejścia AOT i JIT.

    Pętla arytmetyczna na /compute

    Procesor powtarza wyrażenie (result + i * 31) % 1_000_000_007.

    • Node osiągał około 2,835 RPS (p50 na poziomie ok. 27,38 ms)
    • scriptc osiągał około 2,620 RPS (p50 na poziomie ok. 32,21 ms)

    Tutaj JIT w Node wygrał o około 15%. Analiza kodu C wygenerowanego przez scriptc pokazuje, że zmienne lokalne są reprezentowane jako wartości typu C double, a operacja modulo jest realizowana za pomocą funkcji fmod():

    double sc_t11 = fmod(sc_t9, sc_t10);
    

    Wywołanie fmod() wiąże się z kosztami biblioteki dynamicznej. V8 podczas wykonywania obserwuje zachowanie przypominające działanie liczb całkowitych i może zoptymalizować proces dzielenia na 32-bitowe liczby całkowite, co jest tańsze.

    SHA-256 na /hash

    Proces tworzenia hasła odbywa się za pośrednictwem modułu node:crypto.

    • Node: około 9,873 RPS (p50 około 8,45 ms)
    • scriptc: około 28,849 RPS (p50 około 2,97 ms)

    scriptc jest mniej więcej 2,9× szybszy. Node korzysta z języka C++ dwa razy – przy metodach .update() i .digest() – co za każdym razem powoduje dodatkowe obciążenie związane z biblioteką OpenSSL. scriptc łączy te operacje w jedną lokalną funkcję:

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

    Robienie tego bezpośrednio w pamięci lokalnej eliminuje konieczność przesyłania danych między JavaScript a kodem natiwnym.

    6. Rozmiar obrazu kontenera

    Pakowanie dla implementacji w stylu Kubernetes:

    • Obrazy oparte na node:22-slim mają rozmiar około 120 MB
    • Obrazy oparte na debian:bookworm-slim, zawierające tylko dynamicznie łączony binarnik scriptc, mają rozmiar około 80 MB; pakowanie statyczne lub w stylu scratch może osiągnąć rozmiar nawet 25 MB

    7. Istotne ograniczenia scriptc obecnie

    Kompilator pozostaje eksperymentalny i ograniczony:

    Wzory dynamiczne: intensywne użycie any lub bardzo dynamicznego JavaScriptu zmusza do wykorzystania QuickJS (~620KB), co pozbawia aplikację zalet szybkości związanych z kompilacją AOT.

    Ekosystem pakietów: wiele modułów npm polega na mechanizmach wewnętrznych Node lub refleksji, które nie mogą być skompilowane statycznie.

    : jak pokazał przykład z /compute, specjalizacja typów w czasie wykonywania za pomocą JIT jest niedostępna.

    8. Praktyczne wnioski

    Zostań przy Node, gdy:

    • Kluczowe są duże grafy zależności npm
    • Kod ma charakter dynamiczny lub jest luźno typowany
    • Pomoc JIT w pętlach numerycznych o typie dynamicznym jest istotna

    Rozważ scriptc, gdy:

    • Hosty skalowane do zera lub na obrzeżach wymagają czasu uruchomienia poniżej 15 ms oraz ilości pamięci chłodnej RSS poniżej 3 MB
    • Służba przede wszystkim otacza natywne biblioteki (kryptografia, sieciowanie) cienką warstwą API

    Przykłady problemów z reprodukcją znajdują się w tym repozytorium GitHub.