被统称为“编译 TypeScript”的三件事
当开发者搜索如何将 TypeScript 编译为二进制文件时,通常会遇到三种截然不同、却共用同一个说法的技术:
- 转译。
tsc、SWC 和 esbuild 将 TypeScript 转换为 JavaScript。输出的代码仍然需要 Node.js、Bun 或浏览器才能运行,这个过程完全不涉及二进制文件。 - 运行时内嵌。
bun build --compile、deno compile以及 Node.js 的单文件可执行应用(SEA)会把你打包好的 JavaScript 与完整的运行时 拼接在一起。你确实得到了单个文件,但引擎也跟着一起打包了进去,并且 每次进程启动时你的代码依然要被解析并 JIT 编译。 - 提前原生编译。 这正是 Perry 所做的事。TypeScript 先由 SWC 解析,随后解析类型、对 泛型进行单态化,再由 LLVM 生成机器码。链接器产出的是一个普通的可 执行文件——与 Rust、Go 或 C++ 工具链产出的是同一类产物。二进制文件 里完全没有 JavaScript 引擎。
由于没有引擎需要启动,启动时也无需解析任何内容,Perry 编译出的二进 制文件大约在一毫秒内就能启动。这条流水线本身在 TypeScript 原生编译器 页面以及 编译器内部原理中有更详细的介绍。
二进制文件有多大?
体积取决于你引入了什么,因为只有实际用到的代码才会被编译和链接:
- hello world 大约为 330 KB。
- 典型的 CLI 工具在 2–5 MB 之间。
- 引入大型框架(Fastify、mysql2 等)的完整应用大约为 48 MB。
作为对比:Node SEA 可执行文件本质上是 node 二进制文件本身的一份 拷贝,因此在加入你的代码之前,根据平台不同就已经约有 88–118 MB;而 经 Bun 编译的 hello world 在 macOS arm64 上约为 60 MB,在 Linux x64 上约为 100 MB,因为其中内嵌了完整的 Bun 运行时。
Perry 对比 bun build --compile 与 Node SEA
这三者都能给你一个可以直接交给别人的单一文件。除此之外它们是截然不 同的工具,各自都有其适合的场景:
| Perry | bun build --compile | Node SEA | |
|---|---|---|---|
| 产出内容 | AOT 编译的机器码(LLVM) | 打包的 JS + 内嵌的 Bun 运行时 | 注入了你打包脚本的 node 二进制文件拷贝 |
| 执行模型 | 原生代码,没有 JS 引擎 | 运行时 JIT(JavaScriptCore) | 运行时 JIT(V8) |
| Hello-world 大小 | ~330 KB | 约 60 MB(macOS arm64)到 100+ MB(Linux/Windows) | 约 88–118 MB(node 二进制文件的大小) |
| 启动时间 | ~1 ms | ~10 ms | ~30 ms |
| 跨平台编译 | 10 个目标平台,包括从 Linux 交叉编译到 Windows/macOS/iOS | 支持——通过 --target 支持 Linux、Windows、macOS | 不支持——需改为为每个平台复制对应的 node 二进制文件 |
| JS/npm 兼容性 | 不断增长:axios、zod v4、express、fastify、hono 已可原生 编译;其余可通过可选的 V8 回退方案运行 | 完整——因为它本身就是 Bun 运行时 | 完整的 Node 语义;需要预先打包,在 Node 24 LTS 上仅支持 CommonJS |
| 状态 | Pre-1.0 | 稳定 | 在 Node 24 LTS 中为“Active development”稳定级别 |
坦白说:如果你的应用依赖完整的 npm 生态,并且希望零兼容性风险,那么 Bun 和 Node SEA 运行的正是你已经在开发时依赖的引擎语义——这是它们的 优势所在,体积成本对你的部署来说也未必重要。Perry 走的是另一条路。 你得到的是真正的提前编译、更小的二进制文件和毫秒级启动;作为交换, 你采用的是一个 Pre-1.0 的编译器,其 JavaScript 兼容性是被测量并公 开发布的(test262:截至 v0.5.1146,String 为 79%,Array 为 72%),而不是像 V8 那样与生俱来。
详细的正面对比: Perry 对比 Bun 和 Perry 对比 Deno。关于 npm 包如何编译,参见 真实 npm 包与一次一致性扫尾。