
Vercel Labs 在 2026 年 7 月发布了 scriptc,一个将普通 TypeScript 编译为原生可执行文件的编译器。不需要 Node,不需要 V8,二进制文件里不嵌入任何 JavaScript 引擎。一段递归求斐波那契数列的 TypeScript,编译后生成的原生二进制只有 178KB,启动时间约 2 毫秒。同样的代码在 Node 上运行,需要加载约 120MB 的运行时,启动需要约 35 毫秒。
这个项目的核心论点在于:大部分 TypeScript 代码远比生态系统想象的要"静态"。scriptc 逐条语句地判断哪些可以编译为原生代码,哪些不行,并且把结果告诉你。
三层编译策略
scriptc 将程序中的每一个构造明确归入三层之一,层与层之间不存在灰区:
第一层:静态编译(默认)。普通的 TypeScript 代码直接编译为原生机器码,二进制文件中不含任何 JavaScript 引擎。覆盖范围包括:
-
语言层面:带单继承和动态分发的类(在可证明安全时去虚化)、带 JS 捕获语义的闭包、泛型(单态化)、判别联合类型(作为标签值,由 TypeScript 自身的类型窄化驱动)、基于栈式协程的 async/await(保持与 JS 完全一致的调度语义)、带 finally 的异常处理、解构、展开、可选参数/默认参数/rest 参数、getter/setter、迭代器(覆盖字符串/数组/Map/Set)、模板字面量、正则表达式(使用与 QuickJS 相同的 ECMAScript 精确字节码解释器,仅链接到使用正则的二进制文件中)。
-
标准库:UTF-16 精确语义的字符串、保持 JS 精确顺序和标识性的数组/Map/Set、带运行时验证转换的 JSON、Math、类型化数组和 Buffer、带类型化 catch 的 Error 层级。
-
Node API 层:fs(同步和 Promise 版本)、path(逐字节移植)、process、child_process(带管道流)、os、crypto、url/URL、zlib、计时器和信号处理器(运行在无依赖的事件循环上)。服务器栈包括 net、http、https、tls(自带 mbedTLS)、dgram、dns、fs.watch、readline。可以编译真正的代理服务器。
-
fetch 和 WHATWG web 子集(streams、Headers、AbortSignal)运行在同一套原生网络/TLS 栈之上,支持重定向、gzip、AbortSignal.timeout、Node 格式的错误 cause。不依赖 libcurl,不依赖系统 HTTP 库。
第二层:动态运行(可选)。通过 --dynamic 标志启用。一个内嵌的 JavaScript 引擎(quickjs-ng,约 620KB)负责执行无法静态化的部分:npm 依赖的 JavaScript 代码、any 类型的代码。每个从动态层回流到静态代码的值都会在运行时被验证,一个类型撒谎会抛出一个可捕获的 TypeError,而不是导致内存损坏。
第三层:编译期拒绝。所有其他代码在构建时失败,附带一个具体的错误代码、一个代码框、通常还有一个重写提示。没有任何东西会被静默地错误编译。
覆盖率报告:看得见的静态化
scriptc 提供了 coverage 命令,让开发者看到代码中每一条语句的归属:
$ scriptc coverage app.ts
statements analyzed 4481
compile statically 4451 (99%)
blockers:
×2 functions with optional parameters as values SC1090
×1 Promise.reject SC2020在一个 4481 条语句的测试程序中,4451 条(99%)可以静态编译。剩余 3 条被明确标记,每条都附带错误代码。这种设计意味着二进制文件不会静默膨胀——动态层是可选的,覆盖率报告会点明每一个需要它的位置。
架构设计
flowchart LR
TS[TypeScript] -->|tsc: parse + typecheck| L[lowering]
L --> IR[typed IR]
IR --> C[C]
C -->|clang| BIN[native executable]scriptc 的编译管线分为三个包:
-
packages/compiler:前端(通过 tsc API 解析和类型检查 → IR),IR 及其验证器和序列化器,LLVM 和 C 后端。IR 是前后端之间的唯一接口。LLVM 是默认代码生成器(对不在其层级的程序有透明回退),C 是永久参考后端(通过--backend c输出可读的、带源代码行注释的产物)。 -
packages/runtime:C 运行时。引用计数的值(带循环回收器)、栈式协程和事件循环(kqueue)、服务器栈、JS 精确的数字格式化。功能单元按链接门控:二进制文件只为它用到的东西付费。 -
packages/cli:scriptc build | run | coverage。
性能数据
scriptc 在 Apple M 系列芯片上与 Node、Go、Rust、Zig 运行相同工作负载(所有输出逐字节验证一致)进行对比:
| 维度 | scriptc | 对比基准 |
|---|---|---|
| 启动时间 | ~2.4ms | Node: ~47ms;与 Zig 相当,领先 Go/Rust |
| 二进制大小 | 静态 170-200KB,--dynamic + 嵌入依赖约 3MB | Go: ~2MB;Node SEA: 60-100MB |
| 内存占用 (RSS) | 典型 1-4MB | Node: 67-116MB |
| 运行时 | JS 忠实的 f64 语义,大多数工作负载上与系统语言竞争 | 整数推理和所有权分析在路线图上 |
一个 hello-world 二进制约 320KB,启动约 4 毫秒,只链接 libSystem。Node 需要约 120MB 运行时和约 35 毫秒才能打印同一行。
正确性保证
scriptc 用两套机制在每次代码变更时强制正确性:
差分测试。所有语料库程序(800+ 测试)同时在 Node 和原生二进制下运行,stdout、stderr 和退出码必须逐字节匹配。数字格式化是 JS 精确的(最短往返,在百万个双精度浮点数上对 Node 进行模糊测试验证)。服务器用实时客户端驱动程序针对两种实现进行测试。
内存安全通道。整个语料库在 AddressSanitizer 下重新运行,并附带引用计数审计。内存泄漏和释放后使用是构建失败。
对 Node 的有意分歧(有几十个,主要围绕时序内部和错误对象属性)被文档化并编号,没有任何分歧是静默的。
逃生舱口
scriptc 提供了三个高级功能来处理静态编译覆盖不到的场景:
comptime(() => ...) 在构建时运行 TypeScript(在编译器内部的隔离 VM 中),将结果作为字面量烘焙进二进制文件。
Native FFI (--ffi) 将仅含签名的 TypeScript 声明绑定到直接的 C ABI 调用,并链接清单声明的归档文件、目标文件和系统库。边界是显式的、长度限定的。
Checked casts。JSON.parse(...) as Config 会插入运行时验证,抛出一个可捕获的错误并指出出错的路径(如 expected number at $.port, got string)。TypeScript 的 as 是一个承诺,scriptc 验证它。
与 TypeScript 7 的关系
2026 年 7 月 8 日,微软发布了 TypeScript 7.0,用 Go 重写的原生编译器,在完整构建上通常带来 8-12 倍的加速。scriptc 与 TypeScript 7 是正交的方向:TypeScript 7 让编译器本身更快,scriptc 把 TypeScript 程序编译成原生可执行文件。scriptc 使用 TypeScript 自身的 es2025 lib 进行类型检查(当项目有 @types/node 时也会使用),tsconfig.json 控制检查器的严格度。
安装和使用
$ npm install -g scriptc需要 clang(macOS 上随 Xcode Command Line Tools 预装)。macOS arm64 是主要平台,Linux 和 Windows 二进制通过交叉编译构建,各有独立的差分测试通道验证。
基本用法:
$ scriptc run fib.ts # 直接运行
$ scriptc build fib.ts -o fib # 编译为原生二进制
$ scriptc coverage app.ts # 查看静态化覆盖率技术路线
scriptc 目前的短板在整数推理和所有权分析上(路线图中)。当前的运行时使用 JS 忠实的 f64 语义,这在需要精确浮点行为的场景下是优势,但在整数密集型计算上可能不如原生系统语言高效。
项目采用 Apache 2.0 许可证,仓库地址 github.com/vercel-labs/scriptc。这是 Vercel Labs 的实验性项目,截至发文时创建仅 5 天,star 约 500,还没有广泛的生产采用案例。但它提出的"把 TypeScript 当静态语言来编译"的思路,以及差分测试 + AddressSanitizer 的正确性工程实践,对 TypeScript 生态和原生编译领域都有参考价值。