Hotdry.

Article

Perry 原生编译实践:SWC 解析器 + LLVM 后端的类型擦除与静态链接策略

解析 Perry 的 TypeScript 原生编译架构,从 SWC 解析到 LLVM 代码生成的完整管线,探讨类型擦除、NaN-boxing、静态链接与多平台构建的工程化参数。

2026-05-30compilers

TypeScript 长期以来依赖 JavaScript 运行时作为执行环境,无论是 Node.js 的 V8 引擎还是 Deno 的 JavaScriptCore,本质上都是解释执行或 JIT 编译。Perry 的出现打破了这一范式 —— 它直接将 TypeScript 编译为原生机器码,产出独立的可执行文件,无需任何 JavaScript 运行时。这种架构选择带来了显著的性能收益,同时也引入了一系列工程权衡。

编译管线:从源码到机器码

Perry 的编译流程遵循经典的多阶段设计,但针对 TypeScript 的动态特性做了专门优化。整个管线可分为四个阶段:解析、中间表示变换、代码生成和链接。

解析阶段采用 SWC(Speedy Web Compiler)作为前端。SWC 是用 Rust 编写的高性能解析器,能够将 TypeScript/JavaScript 源码快速转换为抽象语法树(AST)。选择 SWC 而非自研解析器,既保证了与 TypeScript 语法的兼容性,又获得了 Rust 生态的性能优势。

中间表示(HIR)阶段是 Perry 的核心创新点。AST 被转换为带类型信息的中间表示,编译器在此阶段执行类型导向的优化。关键变换包括:泛型单态化(Monomorphization)—— 将泛型函数实例化为具体类型的特化版本;闭包转换 —— 将闭包展开为显式的环境捕获结构;以及异步函数展开 —— 将 async/await 转换为状态机。

代码生成阶段委托给 LLVM。HIR 被进一步降维为 LLVM IR,随后由 LLVM 的优化管道处理,最终生成目标平台的机器码。这种设计让 Perry 能够复用 LLVM 成熟的优化基础设施,包括死代码消除、循环优化和向量化。

链接阶段采用静态链接策略,将运行时库、标准库实现和用户代码打包为单一可执行文件。Perry 声称其原生实现的 fspathcrypto 等 Node.js 兼容 API 会在链接时被按需包含,未使用的代码通过 LTO(链接时优化)被剔除。

类型擦除与运行时表示

TypeScript 的类型系统仅在编译期存在,Perry 在生成机器码前会完全擦除类型信息。这种擦除策略与 tsc 的行为一致,但 Perry 更进一步 —— 它利用类型信息进行编译期优化,而非仅仅用于类型检查。

运行时值的表示采用 64-bit NaN-boxing 技术。这是一种在 IEEE 754 双精度浮点数的 NaN 空间中编码其他数据类型的技术。具体而言,JavaScript 的 number 类型直接以双精度浮点存储;其他类型(字符串、对象、布尔值等)的指针被编码在 NaN 的 payload 区域。这种表示法允许在单个 64 位寄存器中存储任意 JavaScript 值,同时保持数值运算的高效性。

NaN-boxing 的优势在于统一的值表示和快速的类型分支判断,但代价是损失了 NaN 的精确语义(JavaScript 中有多种 NaN 表示,但硬件通常统一处理)。Perry 的文档指出,这种权衡对于大多数应用场景是可接受的,因为显式的 NaN 检查在业务代码中较为罕见。

静态链接与运行时权衡

Perry 的默认输出是完全独立的原生可执行文件,典型体积在 2-5MB 之间。这与 Node.js 的~80MB 运行时形成鲜明对比。实现这一体积的关键在于:

  • 按需链接:标准库的每个模块被编译为独立的静态库,链接器仅包含被引用的符号
  • 死代码消除:跨模块的 LTO 能够追踪实际可达的代码路径
  • 无 JIT 开销:移除了解释器和 JIT 编译器的代码体积

然而,这种静态链接策略与 npm 生态存在张力。纯 JavaScript 的 npm 包依赖于 V8 的运行时特性(如 evalFunction 构造函数、动态属性访问模式),无法直接编译为原生码。Perry 提供了 --enable-js-runtime 选项,在需要时嵌入 V8 引擎作为可选运行时。启用后,二进制体积膨胀至 15-20MB,但获得了完整的 npm 兼容性。

这种设计体现了清晰的工程权衡:默认路径追求极致的体积和启动性能,可选路径保留生态兼容性。对于工具类 CLI 应用或资源受限的嵌入式场景,原生模式是理想选择;对于依赖复杂 npm 生态的业务应用,混合模式提供了迁移路径。

多平台构建参数

Perry 支持跨平台编译,目标平台包括 macOS、iOS、iPadOS、Android、Linux、Windows、watchOS、tvOS 和 WebAssembly。命令行接口设计简洁:

# 基础编译
perry compile main.ts

# 指定输出文件名
perry compile main.ts -o myapp

# 启用 V8 运行时以兼容纯 JS npm 包
perry compile main.ts --enable-js-runtime

# 预检代码兼容性
perry check ./src

对于需要原生 UI 的应用,Perry 提供了 25+ 平台原生控件的绑定,包括 AppKit(macOS/iOS)、GTK4(Linux)、Win32(Windows)和 JNI(Android)。这些 UI 组件在编译期被静态链接,运行时直接调用原生 API,避免了 Electron 或 WebView 的内存开销。

性能基准与适用场景

Perry 官方提供的基准测试显示,在 Apple M1 Max 上,其启动时间约为 1ms,相比 Node.js 的~30ms 和 Bun 的~10ms 有显著优势。二进制体积的优势同样明显:2-5MB 的原生可执行文件 vs 80-90MB 的 Node.js/Bun 运行时。

这种性能特征使 Perry 特别适合以下场景:

  • CLI 工具:快速启动对于命令行工具至关重要,Perry 的毫秒级冷启动显著优于 Node.js
  • 资源受限环境:IoT 设备、边缘计算节点等对二进制体积和内存占用敏感的场景
  • 原生 GUI 应用:需要真正原生控件而非 Web 渲染的桌面 / 移动应用
  • 函数计算:冷启动延迟直接影响用户体验的无服务器工作负载

迁移考量与限制

评估 Perry 的落地价值时,需要清醒认识其当前限制:

类型安全边界:Perry 在编译期执行完整的 TypeScript 类型检查,但运行时无类型验证。这意味着来自外部输入(文件、网络、用户输入)的数据必须在编译期被正确标注类型,否则可能引发未定义行为。与 Node.js 相比,Perry 对 "any" 类型的使用更为敏感。

生态兼容性缺口:虽然 Perry 原生实现了 30+ 常用 npm 包(包括 mysql2pgbcryptaxios 等),但 npm 生态的长尾依赖仍可能触发 V8 运行时需求。建议在迁移前使用 perry check 进行兼容性扫描。

调试体验:原生二进制调试与 JavaScript 源码映射存在鸿沟。Perry 的调试支持仍在演进中,复杂问题的定位可能需要阅读生成的 LLVM IR 或汇编代码。

总结

Perry 代表了 TypeScript 工具链向系统编程领域的一次有意义探索。通过 SWC + LLVM 的架构组合,它在保持 TypeScript 开发体验的同时,提供了接近 C++/Rust 的运行时性能。类型擦除与 NaN-boxing 的 runtime 设计、静态链接与可选 V8 的运行时策略,体现了在生态兼容性与性能之间的务实权衡。

对于追求启动速度和资源效率的场景,Perry 提供了比 Node.js 和 Bun 更激进的优化路径。然而,完全的生态兼容性仍需 --enable-js-runtime 选项的妥协。在 TypeScript 原生编译这一新兴领域,Perry 与 Deno 的 deno compile、Bun 的打包方案形成了有趣的竞争格局,开发者可根据具体场景选择最适合的工具链。


参考来源

compilers

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com