scriptc: Compila TypeScript a Ejecutables Nativos sin Runtime

scriptc de Vercel Labs compila TypeScript común en binarios nativos pequeños y rápidos sin Node ni V8. Aprende cómo funciona, qué compila y cómo se compara con Go y Rust.

scriptc: Compila TypeScript a Ejecutables Nativos sin Runtime

Vercel Labs ha lanzado como código abierto scriptc, un compilador de TypeScript a nativo que transforma TypeScript común en ejecutables nativos independientes — sin Node.js, sin V8, sin motor JavaScript requerido en el binario final. ¿El resultado? Binarios que inician en ~2ms, ocupan 170–200KB y consumen 1–4MB de memoria.

El Problema: El Impuesto de Runtime de TypeScript

Los desarrolladores de TypeScript aman la seguridad de tipos y las herramientas del lenguaje, pero distribuir una aplicación Node.js implica empaquetar un runtime pesado. Incluso con las Aplicaciones Ejecutables Únicas (SEA) de Node.js, los binarios pueden inflarse a 60–100MB. Los tiempos de inicio rondan los 47ms en hardware moderno y el uso de memoria a menudo supera los 100MB RSS.

scriptc toma un enfoque diferente: en lugar de distribuir un motor JavaScript, compila TypeScript directamente a código nativo a través de LLVM o un backend C. El resultado es un binario que se comporta byte por byte como Node.js pero sin la sobrecarga.

Cómo Funciona

scriptc utiliza un modelo de compilación de tres niveles:

  1. Compilado estáticamente — El predeterminado. Las construcciones de TypeScript que pueden compilarse a código nativo se compilan directamente. Sin motor involucrado.
  2. Se ejecuta dinámicamente (--dynamic) — Un motor QuickJS-ng incrustado (~620KB) ejecuta código que no puede compilarse estáticamente, como JavaScript incluido en dependencias npm o código con tipo any. Los valores que cruzan de vuelta al código estático se validan en tiempo de ejecución.
  3. Rechazado — Todo lo demás falla con un código de error específico, marco de código y generalmente una sugerencia de reescritura. Nada se compila incorrectamente en silencio.
$ scriptc coverage app.ts

statements analyzed 4481
compile statically 4451 (99%)

blockers:
×2 functions with optional parameters as values SC1090
×1 Promise.reject SC2020

Qué se Compila a Nativo

La superficie de compilación estática de scriptc cubre las características del lenguaje y la biblioteca estándar que los programas reales realmente usan:

Características del Lenguaje

  • Clases con herencia única y despacho dinámico verdadero (desvirtualizado cuando es demostrablemente seguro)
  • Clausuras con semántica de captura de JavaScript
  • Genéricos (monomorfizados)
  • Uniones discriminadas como valores etiquetados impulsados por la propia reducción de TypeScript
  • async/await en fibras basadas en pila con planificación exacta de JavaScript
  • Excepciones con finally
  • Desestructuración, propagación, parámetros opcionales/predeterminados/resto
  • Getters/setters
  • Iteradores sobre cadenas, arreglos, Maps, Sets
  • Literales de plantilla
  • Expresiones regulares (usando el mismo intérprete de bytecode exacto de ECMAScript que QuickJS)

Biblioteca Estándar

  • Cadenas con semántica exacta UTF-16
  • Arreglos, Maps, Sets con orden e identidad exactos de JavaScript
  • JSON con conversiones validadas en tiempo de ejecución
  • Math, arreglos tipados, Buffer
  • Jerarquías de errores con captura tipada

Superficie de API de Node.js

  • fs (síncrono y promesas)
  • path (puerto byte exacto)
  • process, child_process con flujos canalizados
  • os, crypto, url/URL, zlib
  • Temporizadores y manejadores de señales en un bucle de eventos sin dependencias
  • Pila de servidor: net, http, https, tls (mbedTLS integrado), dgram, dns, fs.watch, readline

APIs Web

  • fetch y el subconjunto web WHATWG (flujos, Headers, AbortSignal) sobre la misma pila nativa de net/TLS
  • Redirecciones, gzip, AbortSignal.timeout, causas de error con forma de Node
  • Sin libcurl, sin dependencia HTTP del sistema

Dependencias npm (con --dynamic)

  • Los paquetes se resuelven con el propio algoritmo de Node
  • Verificación de tipos contra sus .d.ts incluidos
  • JavaScript se incrusta en el binario en tiempo de compilación
  • Los binarios nunca leen node_modules en tiempo de ejecución

Comparación de Rendimiento

Medido en Apple M-series contra cargas de trabajo byte-idénticas:

Dimensión scriptc Contexto
Inicio ~2.4ms Node: ~47ms; a la par con Zig, por delante de Go/Rust
Tamaño del binario 170–200KB estático, ~3MB con --dynamic Go: ~2MB; Node SEA: 60–100MB
Memoria (RSS) 1–4MB típico Node: 67–116MB
Tiempo de ejecución Semántica f64 fiel a JS; competitivo con lenguajes de sistemas Inferencia de enteros y análisis de propiedad en la hoja de ruta

Garantías de Corrección

scriptc ejecuta dos mecanismos de verificación en cada cambio:

  1. Pruebas diferenciales — Cada programa del corpus (800+ pruebas) se ejecuta bajo Node y como binario nativo; stdout, stderr y códigos de salida deben coincidir byte por byte. El formato de números es exacto a JS (roundtrip más corto, verificado por fuzzing contra Node en un millón de dobles). Los servidores se prueban con controladores de cliente en vivo contra ambas implementaciones.

  2. Carril de seguridad de memoria — Todo el corpus se vuelve a ejecutar bajo AddressSanitizer con una auditoría de conteo de referencias; las fugas y el uso después de liberación son fallos de compilación.

Vías de Escape

scriptc proporciona varias vías de escape para cuando necesitas salir del modelo estático:

  • comptime(() => ...) — Ejecuta TypeScript en tiempo de compilación (en una VM aislada dentro del compilador) y hornea el resultado en el binario como un literal.
  • FFI nativo (--ffi) — Vincula declaraciones de TypeScript solo de firma a llamadas directas de ABI C y enlaza archivos, objetos y bibliotecas del sistema declarados en manifiesto.
  • --dynamic — Incrusta el motor para dependencias npm y cualquier código. scriptc coverage --dynamic informa exactamente qué declaraciones se ejecutan dónde.
  • Conversiones verificadasJSON.parse(...) as Config inserta una validación en tiempo de ejecución que lanza un error capturable nombrando la ruta ofensiva.

Primeros Pasos

# Instalar
npm install -g scriptc

# Ejecutar un archivo TypeScript directamente
$ cat fib.ts
function fib(n: number): number {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
console.log(fib(30));

$ scriptc run fib.ts
832040

# Construir un binario nativo
$ scriptc build fib.ts && ls -la fib
-rwxr-xr-x 178K fib  # un binario nativo autocontenido, inicio ~2ms

Requisitos: clang (preinstalado con Xcode Command Line Tools). macOS arm64 es la plataforma principal; los binarios de Linux y Windows se construyen mediante compilación cruzada.

Arquitectura

El proyecto está organizado en tres paquetes:

  • packages/compiler — Frontend (API tsc → IR), el IR con validador/serializador, y los backends LLVM y C. El IR es la única interfaz entre los extremos; LLVM es el generador de código predeterminado y C es el backend de referencia.
  • packages/runtime — El runtime C: valores con conteo de referencias y un recolector de ciclos, fibras basadas en pila y el bucle de eventos (kqueue), la pila del servidor, formato de números exacto a JS. Las unidades de características están limitadas por enlace: los binarios pagan solo por lo que usan.
  • packages/cliscriptc build | run | coverage.

Por Qué Esto Importa

scriptc representa un cambio significativo en cómo pensamos sobre el despliegue de TypeScript. En lugar de aceptar el impuesto de runtime de Node.js, ahora puedes compilar tu TypeScript a binarios nativos que inician en milisegundos, usan memoria mínima y son lo suficientemente pequeños para distribuir como archivos únicos. Para funciones serverless, herramientas CLI y computación en el borde, esto podría ser transformador.

El proyecto aún está en etapas tempranas (v0.0.17 al momento de escribir), pero la base es sólida: 800+ pruebas diferenciales, verificación de seguridad de memoria y una hoja de ruta clara para mejoras futuras como inferencia de enteros y análisis de propiedad.

Si estás construyendo aplicaciones TypeScript donde el tiempo de inicio, el uso de memoria o el tamaño del binario importan, scriptc merece una mirada seria.

Fuente

vercel-labs/scriptc: Compilador de TypeScript a Nativo