Vercel 的实验室放出了一个实验性项目:scriptc。它把 TypeScript 和 JavaScript 编译成原生可执行文件——不是打包成 bundle 再套个 Node.js 壳,是真的编译成 C、LLVM IR、汇编、目标文件,最后链成原生 ELF/Mach-O/PE。
一行命令就能看到效果:
scriptc build hello.ts -o hello
./hello ctate
# hello, ctate
产出的 hello 二进制不依赖 Node.js,不依赖任何 JavaScript 引擎。它就是一个原生的可执行文件,和 gcc hello.c -o hello 产出的东西在同一个物种。
TypeScript 作为系统语言的最后一公里
过去两年,TypeScript 已经在悄悄吃掉"系统级 TypeScript"这个生态位。Bun 用 Zig 写了 JS 运行时,Deno 用 Rust 写了 JS 运行时,但它们都还是在"运行"TypeScript——你写完 .ts 文件,运行时在执行时 JIT 编译。scriptc 走的是另一条路:AOT(Ahead-of-Time)编译,写完就编译成机器码,运行时没有解释器、没有 JIT、没有 GC(如果你选静态模式)。
这条路的难点从来不是"能不能编译"——任何图灵完备语言都能编译成机器码。难点是"TypeScript 的类型系统怎么映射到原生类型"。scriptc 用 TypeScript 编译器做解析和类型检查,然后生成一个 typed IR(中间表示),再从 IR 生成 C 或 LLVM IR。这意味着 scriptc 不是"把 JS 当 Python 编译",它真的在利用 TypeScript 的类型信息做静态编译。
关键命令是 scriptc coverage:
$ scriptc coverage hello.ts
statements analyzed 2
compile statically 2 (100%)
fully static — this program has no dynamic remainder.
这个命令告诉你:你的程序里有多少语句能静态编译,有多少必须 fallback 到动态。这是 scriptc 最聪明的设计——它不假装"所有 TypeScript 都能静态编译",而是明确告诉你哪些行能、哪些行不能,以及为什么。
编译产物链:从 IR 到原生可执行文件
scriptc 的编译流水线是一个经典的编译器后端:
TypeScript 源码
↓ (tsc 解析 + 类型检查)
typed IR (JSON)
↓
C 源码 / LLVM IR / 汇编 / 目标文件
↓ (链接)
原生可执行文件 / WebAssembly 模块
每一层都可以单独导出。--emit=ir 出 JSON 格式的 IR,--emit=c 出可读的 C 代码,--emit=llvm 出 LLVM IR,--emit=asm 出汇编,--emit=obj 出目标文件。这种"全链路可观测"的设计不是给普通用户用的,是给编译器开发者用的——你能看到每一层发生了什么,调试起来才有抓手。
最让人意外的是:在 macOS 15+ arm64 上,--emit=asm|obj 不需要 clang、不需要 SDK、不需要任何 C 工具链。scriptc 自带一个 helper 和预编译的 runtime pack,直接生成目标文件。这意味着一个 npm install -g scriptc 就够了,不需要装 Xcode Command Line Tools。
这背后的工程量不小。LLVM 是个庞大的依赖,scriptc 把它打包进了 npm 包里。这种"编译器即 npm 包"的分发方式,和传统编译器(gcc、clang)的"系统级安装"完全不同。它把编译器变成了前端项目的一个 devDependency。
静态 vs 动态:TypeScript 的两副面孔
scriptc 最核心的设计决策是区分"静态可编译"和"动态 fallback"。
静态模式:你的代码只用 TypeScript 的类型系统、纯函数、Node API 的子集。scriptc 把它编译成原生代码,没有 JS 引擎,没有 GC,运行速度和 C 写的等价程序差不多。
动态模式:你的代码用了 any 类型、动态属性访问、eval、某些 npm 包。scriptc 用 --dynamic 选项嵌入 quickjs-ng(一个轻量 JS 引擎),动态部分在 quickjs 里跑。
这两种模式不是互斥的,而是互补的。scriptc coverage 告诉你程序的"静态覆盖率"——100% 意味着可以纯静态编译,不需要嵌入任何 JS 引擎;低于 100% 就需要 --dynamic。
这和 WebAssembly 的设计哲学一脉相承:WASM 也是一个"静态优先、动态 fallback"的编译目标。scriptc 支持 wasm32-wasi 目标,用 Zig 的 WASI libc 生成可移植的 WASI Preview 1 模块。同一份 TypeScript 代码,可以编译成 macOS 上的原生二进制,也可以编译成浏览器里跑的 WASM。
Node API 的静态编译
scriptc 最让人惊讶的能力是:Node API 也能静态编译。README 给了一个 HTTP server 的例子:
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ path: req.url }));
});
server.listen(8080, () => {
console.log("listening on http://localhost:8080");
});
scriptc build server.ts -o server 产出的 server 二进制,不依赖 Node.js,直接 ./server 就能监听 8080 端口。这意味着 node:http 这个核心模块被 scriptc 用原生代码重新实现了。
这和 Bun 的 Bun.serve 路线不同。Bun 是"用 Zig 写一个更快的 Node.js 运行时",scriptc 是"把 Node API 编译成原生代码,不需要运行时"。前者是"更快的解释器",后者是"不需要解释器"。
这条路的对手和参照系
scriptc 不是第一个把高级语言编译成原生的项目。它的参照系至少有三个:
Deno compile:Deno 1.6+ 支持 deno compile,把 TS/JS 打包成单文件可执行。但 Deno 的方案是"把 V8 和源码打包进一个二进制",本质上是自包含的运行时,不是真正的 AOT 编译。二进制体积大(几十 MB),启动速度比 scriptc 的纯静态编译慢。
Bun --compile:Bun 1.1+ 也支持 bun build --compile,思路和 Deno 类似——把 JavaScriptCore 引擎和源码打包。优点是兼容性好(任何 JS 都能跑),缺点是体积大。
AssemblyScript:把 TypeScript 子集编译成 WASM。和 scriptc 最像,但 AssemblyScript 只支持 WASM 目标,不支持原生二进制。而且 AssemblyScript 是"TypeScript 风格的语言",不是 TypeScript 本身——有些 TS 特性不支持。
scriptc 的独特之处:它是唯一一个"用真正的 TypeScript 编译器做类型检查 + 编译成原生二进制 + 支持 Node API 静态编译"的项目。Deno 和 Bun 的方案是"打包运行时",AssemblyScript 是"另一种语言",scriptc 是"真正的 AOT 编译"。
实验性项目的诚实
README 第一段就说清楚了:scriptc 是实验性的,目标平台是 macOS、Linux、Windows、WASM。--emit=obj 是实验性的。外部对象消费是实验性的。AddressSanitizer pipeline 还没对齐。这些"实验性"标签不是免责声明,是诚实的状态报告。
Vercel-labs 是 Vercel 的实验性项目孵化器,不是生产级产品线。scriptc 的作者 ctate(Lee Robinson 的同事)在 Vercel 做编译器研究。这个项目的意义不在于"现在能不能用在生产",而在于"它验证了一条技术路线":TypeScript 可以被当作系统语言来编译,不需要运行时。
如果这条路线走通,影响是深远的。前端开发者用同一种语言写 UI 组件和原生模块,不需要学 Rust 或 C++。边缘计算场景(Cloudflare Workers、Vercel Edge Functions)可以用 TypeScript 写冷启动要求高的函数,编译成原生二进制后启动时间从几十毫秒降到几毫秒。嵌入式场景可以用 TypeScript 写固件,编译成 WASM 跑在资源受限的设备上。
我的看法
scriptc 做的事情让我想起 20 年前的 Java。Java 最初也是"运行时 + 解释器",后来 HotSpot JIT 让它快起来,再后来 GraalVM 让 Java 可以 AOT 编译成原生二进制。TypeScript 正在走同一条路,但速度可能快得多——因为 TypeScript 的类型系统比 Java 更灵活(结构性类型 vs 名义性类型),也因为有 Vercel 这样的公司在背后推动工具链。
"编译器即 npm 包"这个分发模式也值得关注。传统编译器是系统级软件,安装需要 root、需要包管理器、需要处理系统依赖。scriptc 把编译器变成了 npm install -g 就能装的东西,这降低了编译器开发的门槛——任何人都可以写一个编译器后端,发布到 npm 上。
当然,scriptc 现在还很粗糙。--emit=obj 的 ABI marker、scr_runtime_abi_v2 这些东西暴露了它还在快速迭代。但方向是对的:TypeScript 不应该永远是一个"需要运行时"的语言。当它能够被静态编译成原生代码时,它就从一个"前端语言"变成了一个"通用语言"——而通用语言意味着更大的应用场景和更深的生态。
项目地址:https://github.com/vercel-labs/scriptc
安装:npm install -g scriptc(需要 Node.js 24+)
状态:实验性,macOS/Linux/Windows/WASM
讨论回复
加载中...正在加载回复...
推荐
智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。