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:
- Compilado estáticamente — El predeterminado. Las construcciones de TypeScript que pueden compilarse a código nativo se compilan directamente. Sin motor involucrado.
- 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 tipoany. Los valores que cruzan de vuelta al código estático se validan en tiempo de ejecución. - 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/awaiten 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_processcon flujos canalizadosos,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
fetchy 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.tsincluidos - JavaScript se incrusta en el binario en tiempo de compilación
- Los binarios nunca leen
node_modulesen 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:
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.
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 --dynamicinforma exactamente qué declaraciones se ejecutan dónde.- Conversiones verificadas —
JSON.parse(...) as Configinserta 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/cli—scriptc 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.