Inicio / Artículos / TypeScript sin Node ni V8: Pruebas de rendimiento de los binarios nativos de scriptc

TypeScript sin Node ni V8: Pruebas de rendimiento de los binarios nativos de scriptc

Compromisos entre arranque en frío, RSS, ancho de banda y uso de CPU cuando el mismo servidor HTTP de TypeScript se ejecuta como un binario nativo de ScriptC en lugar de con Node.js y V8.

975 palabras

Notas de referencia obtenidas al ejecutar un servicio HTTP en TypeScript como binario nativo en lugar de bajo Node.js con V8.

El JavaScript backend casi siempre necesita un entorno de ejecución. En Node, Deno o Bun, ese entorno suele ser V8 de Google: compilación JIT a código máquina, recolección de basura y un bucle de eventos ajustado para la concurrencia.

Un experimento alternativo busca averiguar si los servidores en TypeScript pueden prescindir de Node, V8 y cualquier otro motor JavaScript, compilándose en su lugar en un binario nativo autónomo.

El compilador experimental de Vercel Labs, scriptc, sigue ese enfoque. TypeScript se convierte a IR LLVM o C intermedio, y luego se finaliza con compiladores comunes como clang.

Ese mismo servicio HTTP en TypeScript se midió en dos configuraciones:

  1. Node.js v22.21.1 ejecutándose con eliminación integrada de tipos
  • scriptc v0.0.32, que genera un binario ELF nativo para x86-64
  • Los resultados a continuación resumen los cambios que ocurrieron tras eliminar V8 de ese servidor.

    1. Carga de trabajo en comparación

    Dada la importancia de la equidad, el servidor utiliza el módulo http estándar de Node sin dependencias adicionales. El texto fuente, la lógica empresarial y los límites HTTP permanecen idénticos en ambos hosts.

    Rutas cubiertas:

    • /health — prueba de disponibilidad sencilla
    • /json — cuerpo JSON estructurado
    • /compute — bucle numérico intensivo
    • /hash — SHA-256 mediante 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. Latencia de arranque en frío

    Las plataformas sin servidor, las funciones de borde y los contenedores que se escalan a cero no tienen en cuenta el tiempo de inicio del proceso. Se midió el tiempo desde la creación del proceso hasta que /health respondiera con éxito, repitiéndose en diez pruebas.

    • Node.js tuvo un promedio de aproximadamente 232.16 ms
    • scriptc tuvo un promedio de aproximadamente 14.23 ms (aproximadamente 16.3× más rápido)

    Al omitir el inicio de V8, el costo de análisis y la configuración de eliminación de tipos, el binario nativo puede responder casi de inmediato.

    3. RSS inactivo y tamaño virtual

    V8 reserva memoria en el heap y buffers JIT de antemano. ¿Cuánta memoria queda reservada mientras el proceso espera?

    • El RSS físico de Node se mantuvo cerca de 70.40 MB
    • El RSS físico de scriptc se mantuvo cerca de 2.55 MB (aproximadamente 27.6× más eficiente)

    El tamaño virtual reveló una diferencia más marcada: Node mostró aproximadamente 21.5 GB de VSZ gracias a la estrategia de mapeo de V8, mientras que scriptc se mantuvo cerca de 4.09 MB.

    4. Ancho de banda de solicitudes y latencia

    oha generó carga durante 10 segundos con 10, 100 y 500 clientes concurrentes.

    Sonda ligera (/health)

    • Con 500 clientes concurrentes, scriptc alcanzó aproximadamente 29,828 RPS, frente a los 9,966 RPS de Node (~2.99×).
    • Con 100 clientes concurrentes, la latencia p50 fue de aproximadamente 2.78 ms en scriptc, en comparación con 8.19 ms en Node.

    Respuestas JSON (/json)

    Incluyendo la serialización, scriptc logró aproximadamente 24,209 RPS, frente a los 10,012 RPS de Node con 100 clientes concurrentes (~2.42×).

    5. Rutas de CPU: por adelantado versus justo a tiempo

    Las rutas orientadas a la CPU muestran dónde difieren AOT y JIT.

    Bucle aritmético en /compute

    El manejador repite (result + i * 31) % 1_000_000_007.

    • Node logró aproximadamente 2,835 RPS (p50 cerca de 27.38 ms)
    • scriptc logró aproximadamente 2,620 RPS (p50 cerca de 32.21 ms)

    Aquí el JIT de Node ganó en aproximadamente un 15%. Al inspeccionar el código C generado por scriptc, se observa que las variables locales son valores de tipo double en C y que el módulo se calcula mediante fmod():

    double sc_t11 = fmod(sc_t9, sc_t10);
    

    La llamada a fmod() implica costos relacionados con las bibliotecas dinámicas. V8 observa un comportamiento similar al de los enteros en tiempo de ejecución y puede especializarse en la división entera nativa de 32 bits, que es más económica.

    SHA-256 en /hash

    El proceso de hash se realiza a través de node:crypto.

    • Node: aproximadamente 9,873 RPS (p50 cerca de 8.45 ms)
    • scriptc: aproximadamente 28,849 RPS (p50 cerca de 2.97 ms)

    scriptc está aproximadamente 2.9× por delante. Node recurre al lenguaje C++ en dos ocasiones para .update() y .digest(), lo que implica un costo adicional por la integración con OpenSSL en cada llamada. scriptc combina todo en una sola llamada nativa:

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

    Al ejecutarse ya en memoria nativa, se elimina el viaje de ida y vuelta entre JavaScript y código nativo.

    6. Tamaño de la imagen del contenedor

    Empaquetado para despliegues al estilo Kubernetes:

    • Las imágenes basadas en node:22-slim tienen un tamaño cercano a 120 MB
    • Las imágenes basadas en debian:bookworm-slim que solo contienen el binario de scriptc enlazado dinámicamente tienen un tamaño cercano a 80 MB; el empaquetado estático o de tipo scratch puede llegar a 25 MB

    7. Límites estrictos de scriptc hoy en día

    El compilador sigue siendo experimental y limitado:

    Patrones dinámicos: el uso intensivo de any o JavaScript altamente dinámico obliga a utilizar QuickJS (~620KB), perdiendo así las ventajas de velocidad en tiempo de compilación previa.

    Ecosistema de paquetes: muchos módulos de npm dependen de componentes internos de Node o de mecanismos de reflexión que no pueden compilarse estáticamente.

    Falta de JIT: como mostró /compute, no existe especialización de tipos en tiempo de ejecución mediante un JIT.

    8. Conclusión práctica

    Manténgase con Node cuando:

    • Son esenciales grandes grafos de dependencias de npm
    • El código es dinámico o de tipo flexible
    • Es importante contar con la ayuda del JIT en bucles numéricos de tipo dinámico

    Considere scriptc cuando:

    • Los hosts de escala a cero o en los bordes necesitan tiempos de inicio inferiores a 15 ms y RSS en estado inactivo inferior a 3 MB
    • El servicio generalmente envuelve bibliotecas nativas (cifrado, redes) en una API ligera

    Los artefactos de reproducción se encuentran en este repositorio de GitHub.