Головна / Статті / TypeScript без Node чи V8: тестування нативних бінарів ScriptC

TypeScript без Node чи V8: тестування нативних бінарів ScriptC

Критичні фактори під час початкового запуску, RSS, пропускна здатність та компроміси щодо використання CPU, коли той самий HTTP-сервер на TypeScript працює як нативний бінарний файл ScriptC замість Node.js із V8.

975 слів

Примітки щодо тестування після запуску HTTP-сервіса на TypeScript у вигляді нативного бінарника замість роботи під Node.js із V8.

JavaScript для бекенду майже завжди потребує середовища виконання. У Node, Deno чи Bun цим середовищем зазвичай є V8 від Google: JIT-компіляція у машинний код, збір сміття та цикл подій, налаштований під конкурентну обробку завдань.

Інший експеримент полягає у перевірці, чи можуть сервери на TypeScript обійтися без Node, V8 та будь-яких інших двигунів JS, компілюючись натомість у один самодостатній нативний бінарник.

Експериментальний компілятор від Vercel Labs scriptc слідує саме цьому підходу. TypeScript спочатку перетворюється у IR LLVM або проміжний код на C, а потім обробляється звичайними компіляторами, такими як clang.

Той самий HTTP-сервіс на TypeScript тестувався у двох конфігураціях:

  1. Node.js v22.21.1, який працює із вбудованою функцією видалення типів
  • scriptc v0.0.32 — створення нативного бінарного файлу ELF x86-64
  • Наведені нижче результати узагальнюють зміни після видалення V8 з цього сервера.

    1. Об’єкти для порівняння

    Чесність була важлива, тому сервер використовує стандартний модуль http від Node без додаткових залежностей. Вихідний текст, бізнес-логіка та межі HTTP залишаються ідентичними на обох хостах.

    Оброблювані маршрути:

    • /health — простий тест готовності
    • /json — структурований тіло у форматі JSON
    • /compute — цикл із числовими операціями
    • /hash — SHA-256 за допомогою 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. Затримка під час початкового запуску

    Платформи без сервера, функції на краю мережі та контейнери, які можуть скоротитися до нуля, не звертають уваги на час запуску процесу. Вимірювання велося від моменту створення процесу до моменту успішної відповіді на запит /health, і це повторювалося десять разів.

    • Node.js показав у середньому приблизно 232,16 мс
    • scriptc показав у середньому приблизно 14,23 мс (приблизно у 16,3 рази швидше)

    Відсутність процесу запуску V8, витрат на парсинг та налаштувань типізації дозволяють нативному бінарнику відповідати майже миттєво.

    3. Спокійний рівень RSS та віртуальний об’єм

    V8 заздалегідь резервує пам’ять heap та буфери JIT. Скільки пам’яті залишається зарезервованою, поки процес чекає?

    • Фізичний рівень RSS для Node знаходився близько 70,40 МБ
    • Фізичний рівень RSS для scriptc знаходився близько 2,55 МБ (приблизно у 27,6 разів менше)

    Віртуальний розмір показав більш чітку картину: Node демонстрував приблизно 21,5 GB VSZ завдяки стратегії мапування V8, тоді як scriptc залишався близько 4,09 MB.

    4. Пропускна здатність та затримка запитів

    oha створював навантаження протягом 10 секунд з 10, 100 та 500 одночасними клієнтами.

    Легкий запит (/health)

    • При 500 одночасних клієнтах scriptc досягав приблизно 29,828 RPS, тоді як Node — 9,966 RPS (~2,99×).
    • При 100 одночасних клієнтах затримка p50 для scriptc становила приблизно 2,78 мс, тоді як для Node — 8,19 мс.

    Відповіді у форматі JSON (/json)

    З урахуванням серіалізації scriptc досягав приблизно 24,209 RPS при 100 одночасних клієнтах, тоді як Node — 10,012 RPS (~2,42×).

    5. Шляхи обробки CPU: заздалегідь versus в момент виконання

    Шляхи, орієнтовані на CPU, показують, де різняться підходи AOT та JIT.

    Арифметичний цикл у /compute

    Обробник повторює виконання (result + i * 31) % 1_000_000_007.

    • Node забезпечує близько 2,835 RPS (середнє значення приблизно 27,38 мс)
    • scriptc забезпечує близько 2,620 RPS (середнє значення приблизно 32,21 мс)

    Тут JIT у Node випереджає приблизно на 15%. Перевірка коду C, створеного scriptc, показує, що локальні змінні представлені як значення типу C double, а операція модулювання виконується за допомогою fmod():

    double sc_t11 = fmod(sc_t9, sc_t10);
    

    Виклик fmod() спричиняє витрати, пов’язані з динамічною бібліотекою. V8 під час виконання спостерігає поведінку, схожу на роботу з цілими числами, і може оптимізувати її під нативне ділення 32-бітних цілих чисел, що є дешевшим.

    SHA-256 у /hash

    Функція генерації хешу виконується через модуль node:crypto.

    • Node: близько 9,873 RPS (p50 близько 8.45 мс)
    • scriptc: близько 28,849 RPS (p50 близько 2.97 мс)

    scriptc випереджає приблизно у 2.9×. Node двічі переходить на C++ для виконання функцій .update() та .digest(), що щоразу призводить до додаткових витрат через інтеграцію з OpenSSL. scriptc об’єднує всі операції в один виклик нативного коду:

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

    Виконання безпосередньо в нативній пам’яті усуває необхідність перемикання між JS та нативним кодом.

    6. Розмір образу контейнера

    Пакування для розгортання у стилі Kubernetes:

    • Образи, засновані на node:22-slim, мають розмір близько 120 MB
    • Образи, засновані на debian:bookworm-slim та містять лише динамічно зв’язаний бінарний файл scriptc, мають розмір близько 80 MB; статичне або спрощене пакування може досягати 25 MB

    7. Критичні обмеження scriptc сьогодні

    Компілятор залишається експериментальним та обмеженим:

    Динамічні патерни: інтенсивне використання any або дуже динамічного JavaScript змушує використовувати QuickJS (~620KB), що позбавляє переваг швидкості AOT.

    Екосистема пакетів: багато модулів npm залежать від внутрішніх механізмів Node чи функцій відображення, які не можна скомпілювати статично.

    Відсутність JIT: як показав /compute, спеціалізація типів під час виконання за допомогою JIT відсутня.

    8. Практичні висновки

    Використовуйте Node, коли:

    • Необхідні великі дерева залежностей npm
    • Код має динамічний чи слабко типовий характер
    • Важлива допомога JIT у циклах з числовими типами

    Розгляньте scriptc, коли:

    • Хости, які мають масштабування до нуля, потребують часу запуску менше 15 мс та об’єму холодного RSS менше 3 МБ
    • Сервіс переважно обгортає нативні бібліотеки (криптографія, мережеві з’єднання) у простий API

    Зразки результатів відтворення знаходяться у цьому репозиторії GitHub.