Vercel 实验室新作 scriptc 刚上线就杀进日榜第 6:TypeScript 从此不用再带 Node 跑?
【免费下载链接】scriptcTypeScript-to-Native Compiler项目地址: https://gitcode.com/GitHub_Trending/sc/scriptc
2026 年 10 月的 GitHub Trending 日榜上,一个来自 Vercel Labs 的实验性项目 scriptc 一上线就冲到了第 6 名,npm 上不到一个月就积累了 5.5k 星、平均每小时 18 次新增的关注速度。这个热度并非营销泡沫——它瞄准的是一个让所有 Node 开发者头疼了十几年的老问题:为什么写 TypeScript 就一定要背着 Node.js 和 node_modules 跑?本文结合社区实测与仓库源码,拆解 scriptc 的编译管线、行为一致性工程,以及它对 Node/Deno/Bun 生态的真实冲击与边界。
一句话定义:TS → 原生二进制的"重编译器"
scriptc 是一个 TypeScript-to-Native 编译器:它复用 TypeScript 官方的解析与类型检查器,利用类型信息把支持子集的 TS/JS 代码直接编译为机器码,产物是不依赖 Node.js、不内置 V8、甚至不依赖任何 JavaScript 引擎的原生可执行文件。仓库自述是 "Compile ordinary TypeScript and JavaScript to small, fast native executables — no Node, no V8, no JavaScript engine in the binary"(见 packages/cli/package.json)。
它不是"打包器"(如 pkg、nexe 那种把 Node 塞进二进制的做法),也不是"TS 转 C 的玩具"。它的编译管线在官方文档中写得很直白(docs/content/docs/how-it-works.mdx):
TypeScript / JavaScript ↓ TypeScript parser and type checker ↓ Lowering and typed IR ──→ 序列化 IR(--emit=ir) ↓ LLVM IR ───────────────→ LLVM 源码(--emit=llvm) ↓ LLVM helper ───────────→ 汇编 / 目标文件(--emit=asm|obj) ↓ Runtime pack 与链接器 ──→ 原生可执行文件关键点在于中间层:typed IR。普通转译器(tsc、Babel、SWC、esbuild)丢掉类型信息只做语法级转换,而 scriptc 把类型信息一直带到 IR:泛型函数按调用点特化、联合类型用 tag 表示、闭包显式声明捕获集。正是这份"带类型的 IR",让后端能在不靠 JIT 预热的情况下直接生成高效的机器码——这是它和"先把 TS 变成 JS 再跑"的整个工具链在架构上的分水岭。
它解决的真实痛点:从 300MB 运行时到单文件二进制
社区对 scriptc 最直观的感知来自一个标志性案例:把 Vercel CLI 从约 300MB 的 Node 运行时压缩进单一原生二进制(CSDN 技术专栏 2026-09 实测报道)。这背后是 Node 分发模式积压多年的三个痛点:
- 分发成本:一个 Node CLI 上线意味着用户机器要有匹配版本的 Node,加上 node_modules 里动辄几百 MB 的依赖树;部署到服务器、容器、边缘函数时,光拷贝运行时和依赖就吃掉大量带宽与冷启动时间。
- 冷启动:V8 引擎初始化、模块图加载、ESM/CJS 转换都发生在程序真正干活之前。
- "一次编译处处运行"的缺失:Node 生态没有官方离线单文件交付物,只能靠 pkg 这类 hack。
scriptc 的做法是绕开 JS 引擎解释层,直接产出原生可执行文件:CLI 工具、数据处理任务、嵌入式场景因此获得毫秒级启动和更小的内存足迹。社区实测(CSDN 2025-12 测评)给出的参考数据是:数值计算快 2-5 倍、冷启动 5-15ms、内存占用低 40-60%——同时 InfoQ 的测试也给出反例(启动快 12 倍但某些运行场景慢 7.5 倍),说明性能并非单边碾压,而是与代码形态强相关,这正是下文要展开的理性边界。
行为一致性:用"Node 即金标准"的差分语料撑起来的工程
任何宣称"替代 Node"的编译器,最大信任危机都是行为不一致。scriptc 在这件事上的工程投入,从仓库测试基建就能看出来。
包结构显示它是一条完整的纵向切分:packages/compiler(前端/IR/LLVM 后端)、native/llvm-codegen(LLVM 22.1.8 辅助器)、packages/runtime(C 运行时:引用计数、循环收集、fiber 事件循环、网络、引擎集成)、packages/runtime-*(各平台预编译对象包)。而它的测试策略是差分语料:tests/harness/differential.test.ts的注释写得很清楚——
每个语料程序都同时跑在 Node 和 scriptc 编译出的原生二进制下,stdout 与 stderr 必须逐字节匹配,退出码必须一致。没有 golden 文件——Node 本身就是期望输出,所以测试不会漂移。
仓库里几百个tests/corpus/语料覆盖模块系统、async/await、事件循环、HTTP、流、Buffer、正则、符号、CJS/ESM 互操作等场景;SCRIPTC_SAN=1时还会把整个语料库用 AddressSanitizer 加引用计数审计重跑一遍,把语料库变成泄漏与 use-after-free 检测器。文档(docs/content/docs/how-it-works.mdx)还披露了三个值得注意的实现选择:
- 并发模型:
async/await用栈式 fiber 实现,原生事件循环在 macOS 用 kqueue、Linux 用 epoll; - TLS:网络层原生实现,TLS 使用 vendored mbedTLS;
- 正则:运行时链接 QuickJS 的正则字节码解释器,而不是自己重写一个。
更系统的是兼容性治理:仓库维护了internal/compatibility/node-v24.json(钉在 Node v24.15.0 的官方 API 全量清单)与配套的static-support.json/dynamic-support.json,把"静态编译支持/动态引擎支持"做成数据化清单,持续对照 Node 官方文档。配合scriptc coverage命令(见 docs/content/docs/coverage.mdx),任何程序都能在编译前得到三分类报告:多少语句静态编译、哪些站点需要动态引擎、哪些不支持——并给出 SC 错误码、代码帧和改写建议。这套"诚实披露 + 数据驱动支持面"的做法,是 scriptc 区别于同类实验编译器最可贵的工程气质。
静态与动态的边界:quickjs-ng 动态岛与"按需链接"的运行时
一个不可能回避的现实是:TS 的any代码和 npm 依赖的 JS 是无法静态编译的。scriptc 的处理不是放弃,而是引入"动态岛":
- 默认静态编译;开启
--dynamic后,把quickjs-ng(约 620KB 级别的轻量 JS 引擎)作为嵌入式引擎链接进二进制,专门执行 npm 依赖与any类型代码(见 docs/content/docs/how-it-works.mdx); - npm 包的 JavaScript 在构建期被嵌入产物,运行时不读
node_modules(README 示例:scriptc build cli.ts --dynamic -o cli,见 README.md); - 静态值与动态值跨界时做运行时校验的复制转换,类型不匹配会抛出可捕获的
TypeError——这甚至比 Node 更严格:JSON.parse(s) as Config在 scriptc 下会真的校验Config形状并报出"expected number at $.port"这种带路径的错误(见 docs/content/docs/limitations.mdx)。
静态侧的运行时则贯彻"只用多少就链接多少":packages/compiler/src/backend/link-plan.ts把链接配方做成数据而非命令行——目标规范决定必选参数,运行时单元矩阵按程序特性(是否用正则、流、HTTP、TLS……)挑选预编译对象与归档,配合 Mach-O/ELF/COFF 段消除把未用代码剪掉。链接器通过--print=native-link-info输出机器可读的 JSON 链接配方(目标平台、对象文件、ABI、运行时源码、FFI 依赖、精确链接顺序),供外部构建工具和 CI 消费——examples/native-object/README.md 就演示了如何用 clang 编译一段 C FFI、再让脚本读取 link-info.json 用 cc 或 ld 两种方式把程序对象、FFI 实现与预编译运行时包链接成可执行文件。对象 ABI 由scr_runtime_abi_*强引用标记,版本不匹配会在链接期直接失败,而不是运行时崩溃。
与此同时,限制清单(docs/content/docs/limitations.mdx)如实列出了边界:静态不支持的部分反射、N-API/V8 原生插件(createRequire加载.node加编译期拒绝)、部分 Date/URL 细节、WeakMap 键类型限制、运行时陷阱不可捕获、process.argv布局与 Node 不同等等。文档明确写着"声明存在于标准库 ≠ 该 API 能编译"(SC2020),并建议先跑coverage再迁移存量项目。这种透明限制恰恰是它作为"实验性"项目的专业姿态。
对 Node/Deno/Bun 生态的真实冲击与边界
回到传播最广的疑问:"TypeScript 从此不用再带 Node 跑?"——准确的答案是:看代码形态。
- 对 Node 本身:scriptc 目前不是、也没打算成为完整的 Node 替代品。它复用 TS 官方类型检查器(依赖清单里同时钉了
typescript@7.0.2与typescript5@5.9.3,见 packages/compiler/package.json),运行时只实现受支持的 API 子集。需要完整 Node API、运行时反射、N-API 插件生态的场景,Node 的地位短期内无法撼动。它真正切走的是 Node 的"脚本/CLI/边缘函数"细分场景——那些不需要完整平台、只求小体积、快启动、易分发的程序。 - 对 Deno/Bun:Deno 与 Bun 的路线是"自带运行时/内置打包",解决的是开发体验与启动速度,但产物仍依赖各自的运行时二进制。scriptc 的路线是彻底消灭运行时依赖,把 TS 直接变成平台原生代码,同时支持 macOS→Linux/Windows/WASI 交叉编译(见 docs/content/docs/platforms.mdx)——这对"编译一次、到处分发"的交付模式是更彻底的回答。作为参照,Perry 是走同一条路线的竞争者,两者在"谁对 JS 语义还原更忠实、支持面更广"上正展开正面竞争。
- 对工具链:scriptc 还展示了"编译器自身用 scriptc 编译"的自我托管验证(
tests/harness/self-hosting-*.test.ts),以及用 Vercel 沙箱跑全量差分语料、LLVM 22 工具链钉版、ASan 内存验证的 CI 工程范本——这套方法对任何想做"编译型 TS"的项目都是可复用的资产。
冷静的一课:热度与边界并存
scriptc 登上日榜第 6 的传播力,来自它用一个极具传播性的叙事回答了 TypeScript 社区积压多年的痛点;而它的长期价值,则要由另一组问题来检验:静态支持面能扩到多宽、quickjs-ng 动态岛的行为偏差能否收敛、链接 ABI 何时稳定(目前scr_runtime_abi仍在 v4/v6 之间迭代,外部对象 ABI 明确标注"experimental")。对于想要尝鲜的开发者,最理性的姿势是:从一个小型 CLI 或数据处理脚本开始,先跑scriptc coverage看清你的代码有多少能静态编译,再决定是否值得为分发效率迁入这条新管线。这个来自 Vercel Labs 的实验,无论最终走向如何,都已经把"TypeScript 的终点是否必须是 JavaScript"这个问题正式摆上了桌面。
【免费下载链接】scriptcTypeScript-to-Native Compiler项目地址: https://gitcode.com/GitHub_Trending/sc/scriptc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考