scriptc: Compile TypeScript to Native Executables with Zero Runtime
Vercel Labs' scriptc compiles ordinary TypeScript into small, fast native binaries without Node or V8. Learn how it works, what compiles, and how it compares to Go and Rust.
scriptc: Compile TypeScript to Native Executables with Zero Runtime
Vercel Labs has open-sourced scriptc, a TypeScript-to-native compiler that transforms ordinary TypeScript into standalone native executables โ no Node.js, no V8, no JavaScript engine required in the final binary. The result? Binaries that start in ~2ms, occupy 170โ200KB, and consume 1โ4MB of memory.
The Problem: TypeScript's Runtime Tax
TypeScript developers love the language's type safety and tooling, but shipping a Node.js application means bundling a heavy runtime. Even with Node.js Single Executable Applications (SEA), binaries can balloon to 60โ100MB. Startup times hover around 47ms on modern hardware, and memory usage often exceeds 100MB RSS.
scriptc takes a different approach: instead of shipping a JavaScript engine, it compiles TypeScript directly to native code via LLVM or a C backend. The result is a binary that behaves byte-for-byte like Node.js but without the overhead.
How It Works
scriptc uses a three-tier compilation model:
- Compiled statically โ The default. TypeScript constructs that can be compiled to native code are compiled directly. No engine involved.
- Runs dynamically (
--dynamic) โ An embedded QuickJS-ng engine (~620KB) executes code that can't be statically compiled, such as npm dependencies' shipped JavaScript orany-typed code. Values crossing back into static code are validated at runtime. - Rejected โ Everything else fails with a specific error code, code frame, and usually a rewrite hint. Nothing is silently miscompiled.
$ scriptc coverage app.ts
statements analyzed 4481
compile statically 4451 (99%)
blockers:
ร2 functions with optional parameters as values SC1090
ร1 Promise.reject SC2020
What Compiles to Native
scriptc's static compilation surface covers the language features and standard library that real programs actually use:
Language Features
- Classes with single inheritance and true dynamic dispatch (devirtualized when provably safe)
- Closures with JavaScript capture semantics
- Generics (monomorphized)
- Discriminated unions as tagged values driven by TypeScript's own narrowing
async/awaiton stackful fibers with JavaScript-exact scheduling- Exceptions with
finally - Destructuring, spread, optional/default/rest parameters
- Getters/setters
- Iterators over strings, arrays, Maps, Sets
- Template literals
- Regular expressions (using the same ECMAScript-exact bytecode interpreter as QuickJS)
Standard Library
- Strings with UTF-16-exact semantics
- Arrays, Maps, Sets with JavaScript-exact ordering and identity
- JSON with runtime-validated casts
Math, typed arrays,Buffer- Error hierarchies with typed catch
Node.js API Surface
fs(sync and promises)path(byte-exact port)process,child_processwith piped streamsos,crypto,url/URL,zlib- Timers and signal handlers on a dependency-free event loop
- Server stack:
net,http,https,tls(vendored mbedTLS),dgram,dns,fs.watch,readline
Web APIs
fetchand the WHATWG web subset (streams, Headers, AbortSignal) over the same native net/TLS stack- Redirects, gzip,
AbortSignal.timeout, Node-shaped error causes - No libcurl, no system HTTP dependency
npm Dependencies (with --dynamic)
- Packages resolve with Node's own algorithm
- Type-check against their shipped
.d.ts - JavaScript is embedded into the binary at build time
- Binaries never read
node_modulesat runtime
Performance Comparison
Measured on Apple M-series against byte-identical workloads:
| Dimension | scriptc | Context |
|---|---|---|
| Startup | ~2.4ms | Node: ~47ms; on par with Zig, ahead of Go/Rust |
| Binary size | 170โ200KB static, ~3MB with --dynamic |
Go: ~2MB; Node SEA: 60โ100MB |
| Memory (RSS) | 1โ4MB typical | Node: 67โ116MB |
| Runtime | JS-faithful f64 semantics; competitive with systems languages | Integer inference and ownership analysis on the roadmap |
Correctness Guarantees
scriptc runs two enforcement mechanisms on every change:
Differential testing โ Every corpus program (800+ tests) runs under Node and as a native binary; stdout, stderr, and exit codes must match byte-for-byte. Number formatting is JS-exact (shortest-roundtrip, fuzz-verified against Node on a million doubles). Servers are tested with live client drivers against both implementations.
Memory-safety lane โ The entire corpus re-runs under AddressSanitizer with a reference-count audit; leaks and use-after-free are build failures.
Escape Hatches
scriptc provides several escape hatches for when you need to break out of the static model:
comptime(() => ...)โ Runs TypeScript at build time (in an isolated VM inside the compiler) and bakes the result into the binary as a literal.- Native FFI (
--ffi) โ Binds signature-only TypeScript declarations to direct C ABI calls and links manifest-declared archives, objects, and system libraries. --dynamicโ Embeds the engine for npm deps and any code.scriptc coverage --dynamicreports exactly which statements run where.- Checked casts โ
JSON.parse(...) as Configinserts a runtime validation that throws a catchable error naming the offending path.
Getting Started
# Install
npm install -g scriptc
# Run a TypeScript file directly
$ 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
# Build a native binary
$ scriptc build fib.ts && ls -la fib
-rwxr-xr-x 178K fib # a self-contained native binary, ~2ms startup
Requirements: clang (preinstalled with Xcode Command Line Tools). macOS arm64 is the primary platform; Linux and Windows binaries build by cross-compilation.
Architecture
The project is organized into three packages:
packages/compilerโ Frontend (tsc API โ IR), the IR with validator/serializer, and the LLVM and C backends. The IR is the only interface between the ends; LLVM is the default code generator, and C is the reference backend.packages/runtimeโ The C runtime: refcounted values with a cycle collector, stackful fibers and the event loop (kqueue), the server stack, JS-exact number formatting. Feature units are link-gated: binaries pay only for what they use.packages/cliโscriptc build | run | coverage.
Why This Matters
scriptc represents a significant shift in how we think about TypeScript deployment. Instead of accepting the Node.js runtime tax, you can now compile your TypeScript to native binaries that start in milliseconds, use minimal memory, and are small enough to distribute as single files. For serverless functions, CLI tools, and edge computing, this could be transformative.
The project is still in early stages (v0.0.17 at the time of writing), but the foundation is solid: 800+ differential tests, memory safety verification, and a clear roadmap for future improvements like integer inference and ownership analysis.
If you're building TypeScript applications where startup time, memory usage, or binary size matter, scriptc is worth a serious look.