I’ve been writing TypeScript for years. I love the type safety, the ecosystem, the fact that I can build anything from a quick script to a full backend with it. But there’s always been this one thing that bugged me: you need Node.js installed to run the damn thing.

Sure, there’s pkg and Node’s SEA (Single Executable App) feature. But those bundle the entire runtime — we’re talking 60 to 100 megabytes — and startup still takes 35 milliseconds or more. For a CLI tool, that’s the difference between feeling instant and feeling sluggish.

Then yesterday, Vercel Labs dropped Scriptc. And honestly, it’s the kind of tool that makes you go “why didn’t anyone do this sooner?”

What Is Scriptc?

Scriptc is a TypeScript-to-native compiler. You write ordinary TypeScript — no special dialect, no annotations, no new syntax — and it compiles down to a native binary that runs without Node, V8, or any JavaScript engine at all.

A simple Fibonacci function becomes a 178-kilobyte executable that starts in about 2.4 milliseconds. Compare that to Node’s 47ms startup and 60+ MB runtime footprint. The difference is night and day.

The project landed on GitHub with over 1,500 stars in its first 24 hours, and for good reason. It’s open source under Apache 2.0, built by Vercel Labs (the same team behind tools like how to build your first MCP server with FastMCP), and it’s genuinely useful right now — not some vaporware roadmap.

Python code on a computer screen showing matplotlib physics simulation
Image: MikeRun via Wikimedia Commons (CC BY-SA 4.0)

Installing Scriptc

Installation is dead simple if you already have Node.js and npm installed:

npm install -g scriptc

That’s it. You’ll need clang installed as well — on macOS that means Xcode Command Line Tools (xcode-select --install), and on Linux you’ll want build-essential or equivalent. macOS arm64 is the primary platform, but Linux and Windows builds are verified by Scriptc’s own differential test lanes.

Once installed, verify it works:

scriptc --version

If you see a version number, you’re good to go.

Your First Scriptc Binary

Let’s start with the classic Fibonacci example. Create a file called fib.ts:

function fib(n: number): number {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

console.log(fib(30));

Now run it with Scriptc:

scriptc run fib.ts

You should see 832040 printed almost instantly. Now let's build a real binary:

scriptc build fib.ts -o fib
./fib

Check the file size:

ls -la fib

You'll see something around 178KB. Compare that to a Node SEA bundle at 60MB. That's not a typo — Scriptc binaries are roughly 300 times smaller. If you're into lightweight, no-dependency CLI tools, you might also enjoy how to use Croc to securely transfer files between computers I covered last week.

Understanding the Three-Tier Model

One of Scriptc's smartest design decisions is how it handles the boundary between code that can and can't be compiled statically. It uses a three-tier model:

Tier 1: Compiled Statically (Default)

Ordinary TypeScript — classes, closures, async/await, generics, the standard library, and a surprising amount of Node's API surface — compiles directly to native code. This includes fs, path, process, http, https, fetch, crypto, and more. The binary contains nothing but machine code.

Tier 2: Runs Dynamically (Opt-in with --dynamic)

Some code can't be compiled statically — npm dependencies that ship plain JavaScript, code using any typed values, or dynamic patterns. For these, Scriptc embeds QuickJS-ng, a compact JavaScript engine (~620KB), via the --dynamic flag. Every value crossing back from dynamic to static code is validated at runtime.

Tier 3: Rejected at Compile Time

Everything else fails with a specific error code, a code frame pointing at the exact line, and usually a rewrite hint. Nothing is ever silently miscompiled.

Working with Coverage Reports

One of my favorite features is the coverage report. Before you add the --dynamic flag, you can check exactly what needs it:

scriptc coverage app.ts

This tells you, statement by statement, what compiles statically and what blocks the rest. A typical result looks like this:

statements analyzed   4481
compile statically    4451  (99%)

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

This kind of transparency is refreshing. You always know exactly what's happening under the hood, and you can decide whether the 620KB engine overhead is worth it for a few dynamic patterns.

Building a Real CLI Tool

Let's build something practical — a simple file line counter. Create linecount.ts:

import { readFileSync } from "fs";

const file = process.argv[2];
if (!file) {
  console.error("Usage: linecount <file>");
  process.exit(1);
}

const content = readFileSync(file, "utf8");
const lines = content.split("\n").length;
console.log(`Lines: ${lines}`);

Build it:

scriptc build linecount.ts -o linecount

Now you have a tiny, fast binary that counts lines in a file. No Node.js needed on the target machine. Share it with a colleague who doesn't have Node installed — it just works.

Escape Hatches for Advanced Use

Scriptc provides three powerful escape hatches when you need to go beyond what compiles statically:

comptime() — Build-Time Execution

The comptime(() => ...) function runs TypeScript at build time (in an isolated VM inside the compiler) and bakes the result into the binary as a literal. For another creative take on developer tooling optimization, check out how I cut your LLM API costs with Toolgz. This is perfect for generating configuration tables, lookup arrays, or pre-computed values:

const primes = comptime(() => {
  // This runs at build time, result is hardcoded in the binary
  return generatePrimeArray(1000);
});

Native FFI (--ffi)

Need to call a C library directly? Scriptc's FFI flag lets you bind signature-only TypeScript declarations to C ABI calls. You declare the function signatures, Scriptc handles the marshalling.

The --dynamic Flag

For npm dependencies that can't be compiled statically, --dynamic embeds QuickJS-ng and runs them in a lightweight interpreter. Packages resolve with Node's own algorithm, typecheck against their .d.ts files, and the JS is embedded directly into the binary — no node_modules needed at runtime.

Performance Comparison: Scriptc vs. Node

The numbers speak for themselves. Here's how Scriptc stacks up against Node.js for CLI tools:

Dimension Scriptc Node.js
Startup time 2.4ms 47ms
Binary size 170-200KB 60-100MB (SEA)
Memory (RSS) 1-4MB 67-116MB
Runtime JS-faithful f64 V8 JIT

For CLI tools, startup time is the killer metric. A tool that starts in 2 milliseconds feels instant. If you enjoy performance numbers, you'll want to see how to benchmark your PC or phone with Geekbench 7 for a broader look at benchmarking. A tool that takes 47 milliseconds — still fast by most standards — already introduces perceptible lag.

When Should You Use Scriptc?

Scriptc isn't meant to replace Node.js for everything. Here's where it shines:

  • CLI tools and command-line utilities — the primary use case. Small binaries, instant startup, no runtime dependencies on the target machine.
  • DevOps scripts and automation — deploy a single binary to a server without managing Node versions.
  • Performance-sensitive microservices — if you're writing a service in TypeScript and every millisecond of cold start matters (hello, serverless functions), Scriptc is worth a look.
  • Distribution to non-Node users — share a tool with someone who doesn't have Node installed.

When you should probably stick with regular Node:

  • Full web applications — Scriptc doesn't replace a full Node.js server for complex web apps.
  • Code that heavily uses dynamic patterns — if most of your code needs the --dynamic flag, the 620KB engine overhead starts eating into the size advantage.
  • Early-stage projects — Scriptc is brand new. For mission-critical production systems, you might want to wait for the ecosystem to mature.

Correctness and Safety

One concern you might have: does compiled TypeScript behave the same as running it in Node? Scriptc takes this seriously. It has two enforcement mechanisms:

  1. Differential testing — every program in their 800+ test corpus runs under Node AND as a native binary. stdout, stderr, and exit codes must match byte-for-byte. Number formatting is verified against Node on a million doubles.
  2. Memory-safety lane — the entire corpus re-runs under AddressSanitizer with a reference-count audit. Leaks and use-after-free are build failures.

The deliberate divergences from Node (there are a few dozen, mostly around timing internals) are documented and numbered. Nothing diverges silently.

Getting Started Today

The best way to understand Scriptc is to try it. Install it, compile a simple TypeScript file, and check the binary size. Run it on a machine without Node and watch it work. Then run scriptc coverage on one of your existing TypeScript CLI tools to see what percentage compiles statically — you might be surprised how much of your code is already ready for native compilation.

Scriptc is fresh out of the lab — it's version 0.x, it's experimental, and it has rough edges. But the core idea is solid, the execution is impressive, and it's already useful for real work. I've been burned by premature tool hype before, but this one feels different. It's solving a real problem that anyone who's shipped a TypeScript CLI has felt, and it's solving it well.

The project is at github.com/vercel-labs/scriptc and the official site is scriptc.dev with full documentation.

Filed under Tech & Gadgets
Last Update: July 28, 2026 by Felix AlterEgo
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Newest
Oldest Most Voted