DEV Community

Anup Karanjkar
Anup Karanjkar

Posted on Edited on Originally published at wowhow.cloud

scriptc by Vercel: TypeScript to a 370 KB Native Binary

A TypeScript program compiled with scriptc ships as a 370 KB binary that starts in about 1.5 ms and idles at 2.5 MiB, yet the same byte-array benchmark runs roughly 7.5 times slower than Node 24. Vercel Labs created the repository on 22 July 2026, licensed it Apache 2.0, and the project passed 5,000 GitHub stars by late September (per InfoQ, September 2026). Both halves of that sentence are true at once, and the gap between them decides whether the tool fits your workload.

Short answer: scriptc wins when a process lives for milliseconds and loses when it lives for seconds. Using the published numbers, a program whose Node compute time is under roughly 9 ms finishes sooner as a scriptc binary. Above that, the 7.5x compute penalty eats the startup gain. CLIs, git hooks, edge shims and sidecar utilities sit on the winning side. Servers, build tools and data crunchers sit on the losing side. The compiler is also at version 0.0.16, so treat every number below as a snapshot of an experiment, not a promise.

What scriptc actually is

scriptc is a TypeScript-to-native compiler. It uses the real TypeScript compiler for parsing and type checking, lowers the result to a typed intermediate representation, and then emits readable C, LLVM IR, assembly, object files, native executables or WebAssembly targeting WASI Preview 1. It runs on macOS, Linux and Windows and needs Node.js 24 or newer to install (per the vercel-labs/scriptc repository).

npm install -g scriptc
Enter fullscreen mode Exit fullscreen mode

The design has three tiers, and the tier you land in decides everything about performance. One extra property is worth knowing too.

  • Static (default). Ahead-of-time compilation of code the type system can fully resolve. This is where the 1.78 ms startup comes from. Constructs the compiler cannot lower are rejected with an SC error code and a rewrite hint.

  • Dynamic (--dynamic). Embeds quickjs-ng, about 620 KB, to run npm packages and code typed with any. You get compatibility and lose most of the speed.

  • Static npm (--npm-static). Experimental. Compiles named npm packages statically instead of interpreting them.

  • Dual output. Because the C is readable, you can inspect what the compiler produced. That helps when a rewrite hint does not make sense.

The behavioural differences from Node are documented, not hidden. Strings are UTF-8. Memory uses reference counting instead of a garbage collector. Object.keys follows declaration order. process.argv[0] equals "scriptc". The project runs differential tests against Node and compares stdout, stderr and exit code byte for byte, which is the right way to prove a compiler preserves behaviour.

The numbers, and what each one measures

Three different benchmarks circulate, and mixing them produces nonsense. The table keeps them apart.

Source Metric scriptc Comparison

| InfoQ benchmark, v0.0.16 | Median CLI startup | 1.78 ms | Bun 1.3.12: 21.29 ms. Node 24.18.0: 61.78 ms |

| InfoQ benchmark | Idle memory, framework-free node:http server | 1.9 MiB | Not stated for runtimes |

| Launch-day demo | Simple CLI | 178 KB, 2.4 ms start | None |

| Hacker News developer, byte-array test | Startup | 1.5 ms | Node 24: 18.6 ms |

| Same test | Memory | 2.5 MiB | Node 24: 181 MiB |

| Same test | Compute | About 7.5x slower | Node 24 baseline |

| Same test | Executable size | About 370 KB | Not applicable |

Read the startup row first. Against Node 24.18.0, 1.78 ms is a 34.7x improvement. Against Bun 1.3.12 it is 12x. The Hacker News test reports 12.4x against a different Node build and a lighter workload, which is why its absolute numbers differ. Memory is the more dramatic figure: 2.5 MiB versus 181 MiB is a 72x reduction, and that is the number that matters if you run thousands of tiny processes or pack containers tightly.

Then read the compute row. Seven and a half times slower is not a rounding error. It is the cost of compiling a dynamic language to code that cannot speculate the way V8's optimizing compiler does.

Where the trade breaks

The clearest failure case is a web server. Hono, a popular framework, needed --dynamic to compile, which put about 62% of the server inside the QuickJS interpreter. The result was 18.4k requests per second against 70.5k on Bun, a 3.8x deficit (per byteiota, September 2026). Memory was still tiny, so a low-traffic internal service might accept that. A public API will not.

Two critics explain why the ceiling is low. Filip Pizlo argued that representing every number as a float and deferring integer inference skips "half the problem of fast JS", and that QuickJS is a poor fit for the fallback tier. He is describing an architecture limit, not a bug that version 0.0.17 fixes.

The ecosystem report is blunter. One developer saw hundreds of errors on every local project they tried. Remo Jansen tried compiling the TypeScript 6 compiler itself and hit an internal compiler error. When his smaller test did run, cold start was 3.6 ms against Node's 48.9 ms, but compute took 2.33 seconds. That is the pattern in one data point: instant to start, slow to finish.

And there is the longevity question. Vercel's earlier zerolang experiment went dormant within weeks of launch, and nothing in an Apache 2.0 license prevents the same here. Simon Willison's counterpoint is the strongest argument for trying it anyway: small, fast binaries without writing C or Rust "seems like a valuable capability", and coding agents landed 918,000 lines in the repository in a single week, so the project is moving faster than most hobby compilers.

The break-even math

Here is the model I use to decide. A short-lived process costs startup plus compute. For Node, take the InfoQ startup of 61.78 ms plus compute time w. For scriptc, take 1.78 ms plus 7.5 times w. The two lines cross where 60 equals 6.5 times w, so w is about 9.2 ms. The assumption is crude, because it combines numbers from two separate benchmarks, but it gives you a defensible order of magnitude.

function totalMs(computeMsOnNode, runtime) {
  if (runtime === 'node') return 61.78 + computeMsOnNode
  return 1.78 + 7.5 * computeMsOnNode
}
Enter fullscreen mode Exit fullscreen mode
Node compute time Node total scriptc total Winner

| 1 ms | 62.78 ms | 9.28 ms | scriptc, 6.8x faster |

| 5 ms | 66.78 ms | 39.28 ms | scriptc, 1.7x faster |

| 9.2 ms | 71 ms | 71 ms | Tie |

| 50 ms | 111.78 ms | 376.78 ms | Node, 3.4x faster |

| 500 ms | 561.78 ms | 3,751.78 ms | Node, 6.7x faster |

Now apply volume. A git hook that runs 10,000 times a day with 1 ms of compute spends 627.8 seconds of wall time on Node and 92.8 seconds on scriptc, a saving of about 8.9 minutes daily. For one developer that is nothing. For a CI fleet running millions of invocations it is real money, and the 72x memory gap lets you run more processes per machine.

Measure your own program before believing any of this. This command compares a Node script against its compiled twin with warm-up runs:

hyperfine --warmup 5 'node dist/cli.js --version' './cli --version'
Enter fullscreen mode Exit fullscreen mode

If you are unsure how much compute your tool does, paste its input into the Token Counter to size prompts for any LLM step in the pipeline, and map the import graph with the Codebase Graph Visualizer to see how many npm packages would force --dynamic. A tool with a short dependency list compiles statically. A tool that imports forty packages will not.

Who should adopt it now

Use scriptc if three conditions hold. The program is a CLI, hook or sidecar. It runs for under about 10 ms of compute. And you can pin the toolchain version and accept that behaviour may shift between releases. That profile covers formatters for small files, commit-message linters, environment checkers and installers where a single 370 KB file with no runtime is the whole point.

Skip it, for now, if you ship a server, a bundler, a test runner or anything that imports a large dependency tree. Wait for the static npm path to mature before betting a production service on it.

One practical habit helps either way. Write the compile target and the Node-divergence list into your repository's instruction file so coding agents stop assuming Node semantics. The CLAUDE.md Production Rules pack has the structure, and the Claude Code CLAUDE.md Starter Templates Pack gives you a TypeScript template to edit. The pattern of putting hard constraints in files rather than prompts is the same one we covered in the macOS 27 offline CLI playbook.

Run a one-afternoon trial before you commit. Pick the smallest CLI in your repository, one with few imports and no dynamic requires. Compile it in the default static tier and read every SC error the compiler prints. Each carries a rewrite hint, and the count tells you how much of your code relies on features the static tier refuses. Then run your existing test suite against the binary and diff stdout, stderr and exit code against the Node build. That is the same comparison the project's own differential tests make.

If the diff is empty and the SC error count is zero, benchmark with the command above. If either number is large, stop. You have learned the cost of adoption for roughly an hour of work.

Plan for the documented divergences as well. UTF-8 strings change how code behaves when it indexes into non-ASCII text. Reference counting means cyclic references can leak where a garbage collector would have reclaimed them. Declaration-order Object.keys can differ from what a library assumed. A test that exercises your real data surfaces each one. None is a reason to avoid the tool, and all of them are reasons not to ship a binary you have not diffed.

Quick answers

Is scriptc production-ready?

No. The benchmarked version is 0.0.16, the project is labelled experimental, and reviewers report hundreds of compile errors on real projects. Use it for contained utilities you can fall back from.

How fast does a scriptc binary start?

The InfoQ benchmark measured a median of 1.78 ms, against 21.29 ms for Bun 1.3.12 and 61.78 ms for Node 24.18.0. An independent test measured 1.5 ms against 18.6 ms for Node.

Why is compute slower than Node?

The compiler uses floats for all numbers and defers integer inference, and it has no speculative optimizing JIT. An independent byte-array test measured about 7.5x slower than Node 24.

Does it run npm packages?

Only through --dynamic, which embeds a 620 KB QuickJS interpreter and runs that code slowly. An experimental --npm-static flag compiles named packages statically.

Every product mentioned is available at wowhow.cloud — pay once, ship forever.


Originally published at wowhow.cloud

Top comments (0)