正在刊行长文 · Essay
2026-07-28所有内容
随机比特 · Random Bits

TypeScript 终于能离开 Node 了,但你的 npm 依赖还走不了

2026-07-28AI Engineering / Systemsrbits.uk
TypeScript 终于能离开 Node 了,但你的 npm 依赖还走不了

同一段只有几行的 TypeScript 小程序,在 Node 中启动中位数约 30.3 毫秒、最大 RSS 约 45.5MB;编译为原生可执行文件后,同一环境下中位数降到约 1.7 毫秒与 1.9MB。随后我加入一个常见的 npm 依赖,编译却立刻失败。

上述数字来自作者在 OpenCloudOS x86_64、Scriptc 0.0.17、clang 17 上的简单样本对照——启动各跑 80 次、内存各跑 20 次。它只说明短生命周期小程序的启动与常驻成本,不能当作真实服务吞吐结论。官方在 Apple M 系列上另有自报数据:静态二进制约 170–200KB、启动约 2.4ms、典型 RSS 1–4MB。方向一致,但环境不同,不可互相替代。

原生编译已经可用

Scriptc 把普通 TypeScript 经类型检查与中间表示,编译为不含 Node、也不含 V8 的原生程序;默认静态模式不携带 JavaScript 引擎。需要说清的是:产物是无 Node/V8 的原生 ELF,仍会链接系统 C 库;npm 项目也无法全部一键迁移。

一个关键细节说明它做了真正的编译。JSON.parse(...) as Order 会在运行时插入校验;错误输入会得到类似 expected number at $.items[0].price, got string 的路径化报错。类型断言在这里从编译期注释变成了可执行的边界检查。

官方公开了三层边界:能静态编译为原生代码;使用 --dynamic 时,由内嵌 quickjs-ng 运行 npm 依赖与动态代码;其余能力明确拒绝并给出诊断。本地静态样本中,hello.ts 与带订单结构的小程序均可编译通过,未精简产物分别约 379KB 与 380KB。

TypeScript 原生编译的静态与动态边界

npm 依赖仍是迁移边界

语法已经不再是首要障碍,npm 生态与动态 JavaScript 才决定迁移范围。本地在静态覆盖率 100% 的样本里引入 kleur 后,覆盖率直接降为 0%;Scriptc 以 SC2013 明确拒绝,并提示改用 --dynamic。动态覆盖报告显示 100% 走动态路径、无剩余 blocker,但本机在链接 quickjs 静态库时失败:relocation R_X86_64_32S against '.rodata' can not be used when making a PIE object。这只是 Scriptc 0.0.17 在 OpenCloudOS x86_64 + clang 17 上的结果,不能外推为全部 Linux、macOS 或后续版本。

这次实测改变了我的判断:TypeScript 原生化的范围由 npm 依赖和动态 JavaScript 决定。当前应优先迁移静态小核心,整个 Node 应用不宜直接搬迁。

对 CLI、Serverless 辅助程序、CI 自动化一类启动敏感、生命周期短的程序,可先跑静态覆盖率,再按三条路径选型:静态覆盖高且对启动与 RSS 敏感,值得试原生化;npm 依赖重、动态行为多,继续留在 Node;主应用复杂但能隔离一小块纯 TypeScript 核心,只抽离核心验证,不整体搬家。

随机比特公众号二维码
公众号 · 随机比特
从 AI 工具热闹里拆工程真相

写边界、控制面、上下文、成本与安全。