scriptc : Compiler TypeScript en exécutables natifs sans runtime

scriptc de Vercel Labs compile du TypeScript ordinaire en binaires natifs petits et rapides, sans Node ni V8. Découvrez comment cela fonctionne, ce qui est compilé et comment il se compare à Go et Rust.

scriptc : Compiler TypeScript en exécutables natifs sans runtime

Vercel Labs a open-sourcé scriptc, un compilateur TypeScript vers natif qui transforme du TypeScript ordinaire en exécutables natifs autonomes — pas de Node.js, pas de V8, pas de moteur JavaScript nécessaire dans le binaire final. Le résultat ? Des binaires qui démarrent en ~2ms, occupent 170–200 Ko et consomment 1–4 Mo de mémoire.

Le problème : la taxe d'exécution de TypeScript

Les développeurs TypeScript adorent la sécurité de type et les outils du langage, mais déployer une application Node.js signifie inclure un runtime lourd. Même avec les applications exécutables uniques (SEA) de Node.js, les binaires peuvent gonfler jusqu'à 60–100 Mo. Les temps de démarrage tournent autour de 47 ms sur du matériel moderne, et l'utilisation de la mémoire dépasse souvent 100 Mo RSS.

scriptc adopte une approche différente : au lieu d'embarquer un moteur JavaScript, il compile le TypeScript directement en code natif via LLVM ou un backend C. Le résultat est un binaire qui se comporte octet pour octet comme Node.js mais sans la surcharge.

Comment ça marche

scriptc utilise un modèle de compilation à trois niveaux :

  1. Compilé statiquement — Par défaut. Les constructions TypeScript qui peuvent être compilées en code natif sont compilées directement. Aucun moteur impliqué.
  2. S'exécute dynamiquement (--dynamic) — Un moteur QuickJS-ng embarqué (~620 Ko) exécute le code qui ne peut pas être compilé statiquement, comme le JavaScript fourni par les dépendances npm ou le code typé any. Les valeurs qui traversent vers le code statique sont validées à l'exécution.
  3. Rejeté — Tout le reste échoue avec un code d'erreur spécifique, un cadre de code, et généralement un indice de réécriture. Rien n'est silencieusement mal compilé.
$ scriptc coverage app.ts

statements analyzed 4481
compile statically 4451 (99%)

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

Ce qui est compilé en natif

La surface de compilation statique de scriptc couvre les fonctionnalités du langage et la bibliothèque standard que les programmes réels utilisent réellement :

Fonctionnalités du langage

  • Classes avec héritage simple et dispatch dynamique réel (dévirtualisé quand c'est prouvablement sûr)
  • Fermetures avec sémantique de capture JavaScript
  • Génériques (monomorphisés)
  • Unions discriminées comme valeurs étiquetées pilotées par le propre rétrécissement de TypeScript
  • async/await sur fibres avec pile et ordonnancement exact JavaScript
  • Exceptions avec finally
  • Destructuration, spread, paramètres optionnels/par défaut/reste
  • Getters/setters
  • Itérateurs sur chaînes, tableaux, Maps, Sets
  • Littéraux de gabarit
  • Expressions régulières (utilisant le même interpréteur de bytecode ECMAScript exact que QuickJS)

Bibliothèque standard

  • Chaînes avec sémantique UTF-16 exacte
  • Tableaux, Maps, Sets avec ordre et identité exacts JavaScript
  • JSON avec conversions validées à l'exécution
  • Math, tableaux typés, Buffer
  • Hiérarchies d'erreurs avec catch typé

Surface API Node.js

  • fs (synchrone et promesses)
  • path (port octet exact)
  • process, child_process avec flux pipés
  • os, crypto, url/URL, zlib
  • Timers et gestionnaires de signaux sur une boucle d'événements sans dépendance
  • Pile serveur : net, http, https, tls (mbedTLS fourni), dgram, dns, fs.watch, readline

API Web

  • fetch et le sous-ensemble web WHATWG (flux, Headers, AbortSignal) sur la même pile native net/TLS
  • Redirections, gzip, AbortSignal.timeout, causes d'erreur en forme Node
  • Pas de libcurl, pas de dépendance HTTP système

Dépendances npm (avec --dynamic)

  • Les paquets résolvent avec l'algorithme propre à Node
  • Vérification de type contre leurs .d.ts fournis
  • Le JavaScript est intégré dans le binaire au moment de la construction
  • Les binaires ne lisent jamais node_modules à l'exécution

Comparaison des performances

Mesuré sur Apple M-series contre des charges de travail octet-identiques :

Dimension scriptc Contexte
Démarrage ~2,4 ms Node : ~47 ms ; au niveau de Zig, devant Go/Rust
Taille du binaire 170–200 Ko statique, ~3 Mo avec --dynamic Go : ~2 Mo ; Node SEA : 60–100 Mo
Mémoire (RSS) 1–4 Mo typique Node : 67–116 Mo
Exécution Sémantique f64 fidèle à JS ; compétitif avec les langages systèmes Inférence d'entiers et analyse de possession sur la feuille de route

Garanties de correction

scriptc exécute deux mécanismes de contrôle sur chaque changement :

  1. Tests différentiels — Chaque programme du corpus (800+ tests) s'exécute sous Node et comme binaire natif ; stdout, stderr et codes de sortie doivent correspondre octet pour octet. Le formatage des nombres est exact JS (arrondi le plus court, vérifié par fuzz contre Node sur un million de doubles). Les serveurs sont testés avec des pilotes clients en direct contre les deux implémentations.

  2. Voie de sécurité mémoire — L'ensemble du corpus est réexécuté sous AddressSanitizer avec un audit de comptage de références ; les fuites et les use-after-free sont des échecs de construction.

Échappatoires

scriptc fournit plusieurs échappatoires pour quand vous avez besoin de sortir du modèle statique :

  • comptime(() => ...) — Exécute TypeScript au moment de la construction (dans une VM isolée à l'intérieur du compilateur) et intègre le résultat dans le binaire comme un littéral.
  • FFI natif (--ffi) — Lie des déclarations TypeScript de signature uniquement à des appels directs ABI C et lie des archives, objets et bibliothèques système déclarés dans un manifeste.
  • --dynamic — Embarque le moteur pour les dépendances npm et tout code. scriptc coverage --dynamic rapporte exactement quelles instructions s'exécutent où.
  • Conversions vérifiéesJSON.parse(...) as Config insère une validation à l'exécution qui lance une erreur attrapable nommant le chemin fautif.

Pour commencer

# Installation
npm install -g scriptc

# Exécuter un fichier TypeScript directement
$ 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

# Construire un binaire natif
$ scriptc build fib.ts && ls -la fib
-rwxr-xr-x 178K fib  # un binaire natif autonome, ~2ms de démarrage

Prérequis : clang (préinstallé avec Xcode Command Line Tools). macOS arm64 est la plateforme principale ; les binaires Linux et Windows sont construits par compilation croisée.

Architecture

Le projet est organisé en trois paquets :

  • packages/compiler — Frontend (API tsc → IR), l'IR avec validateur/sérialiseur, et les backends LLVM et C. L'IR est la seule interface entre les extrémités ; LLVM est le générateur de code par défaut, et C est le backend de référence.
  • packages/runtime — Le runtime C : valeurs comptées par référence avec un collecteur de cycles, fibres avec pile et la boucle d'événements (kqueue), la pile serveur, formatage de nombres exact JS. Les unités de fonctionnalité sont liées par liens : les binaires paient seulement pour ce qu'ils utilisent.
  • packages/cliscriptc build | run | coverage.

Pourquoi c'est important

scriptc représente un changement significatif dans notre façon de penser le déploiement TypeScript. Au lieu d'accepter la taxe d'exécution Node.js, vous pouvez maintenant compiler votre TypeScript en binaires natifs qui démarrent en millisecondes, utilisent un minimum de mémoire, et sont assez petits pour être distribués comme fichiers uniques. Pour les fonctions serverless, les outils CLI et l'informatique en périphérie, cela pourrait être transformateur.

Le projet en est encore à ses débuts (v0.0.17 au moment de la rédaction), mais les fondations sont solides : 800+ tests différentiels, vérification de la sécurité mémoire, et une feuille de route claire pour les améliorations futures comme l'inférence d'entiers et l'analyse de possession.

Si vous construisez des applications TypeScript où le temps de démarrage, l'utilisation de la mémoire ou la taille du binaire comptent, scriptc mérite un examen sérieux.

Source

vercel-labs/scriptc : Compilateur TypeScript vers natif