将 JS 打包为可执行程序
把 JavaScript 打包成可执行程序,本质上是把 源码、依赖 和 运行时 一起封进一个产物里。
这类工具看起来都在做同一件事,但差异非常大。有的方案是在打包 Node.js 运行时,有的方案是在打包 Bun / Deno 运行时,也有的方案更接近把脚本编译进一个很小的 JS 引擎里。
所以看这类方案时,最重要的不是“能不能打包”,而是:
- 它基于哪个运行时
- 它和 Node / npm 生态的距离有多近
- 它打出来的产物到底有多大
- 它现在是不是一个值得投入的新选择
一个最小基准
为了避免只停留在概念层面,我在本机上直接做了一个最小测试。
测试脚本只有一行:
console.log('hello, world')测试环境:macOS / Apple Silicon。
下面的大小,都是这个最小脚本打成可执行文件之后的实际结果。
对比
| 方案 | 运行时 | 产物大小(macOS) | Node / npm 兼容性 | 整体判断 |
|---|---|---|---|---|
qjsc | QuickJS | 0.70 MB | 很低 | 体积优势最明显 |
pkg | Node.js | 44.4 MB | 较好 | 经典 Node 路线 |
bun build --compile | Bun | 58.2 MB | 中等 | 很直接,体验新 |
deno compile | Deno | 76.6 MB | 中等偏高 | 分发能力完整 |
| Node.js SEA | Node.js | 106.3 MB | 好 | 官方路线,但产物偏大 |
nexe | Node.js | - | 较好 | 本机测试未成功产出 |
几点很直观:
- QuickJS 和其他几种方案不是一个量级,体积最小。
- 只要进入 Node / Bun / Deno 这一类“自带完整运行时”的路线,产物就会立刻来到几十 MB。
- Node.js SEA 作为官方方案,在这个最小例子里反而是最大的。
pkg
vercel/pkg 是最常被提到的方案之一。它的核心思路很直接:把 Node.js 运行时 和项目一起封进一个可执行文件里。因此它对 Node CLI 开发者来说很容易理解,迁移成本也比较低。
优势:
- 对传统 Node CLI 项目比较友好
- 社区资料多,历史使用案例多
- 体积在几种“完整运行时方案”里不算最大
特点:
- 它代表的是一类比较经典的 Node 打包思路
- 对 CommonJS 项目通常更自然
- 遇到动态加载、资源文件、原生模块时,处理成本会上升
我的判断:
今天再看 pkg,它更像一个老牌、可参考、适合存量项目的答案,而不是新项目的第一推荐。
Bun
oven-sh/bun 提供的bun build --compile,是这几年讨论度非常高的一条路线。它的特点是很“直给”:你可以把脚本和 Bun 运行时 一起编译成一个可执行文件。整体感受和 pkg 很不一样,少了很多“历史包袱”,更像现代工具链的一部分。
优势:
- 非常直接
- 对 TS、ESM、现代工程风格更友好
- 在完整运行时方案里,体积比 Deno 和 SEA 更克制
特点:
- 它绑定的是 Bun,不是原生 Node
- 它更适合现代脚本工具,而不是完全复刻 Node 运行时行为
- 它的优点不是“最官方”,而是“足够直接”
我的判断:
如果从今天的视角看,Bun 是这类问题里最省心、也最值得优先关注的方案之一。
Deno
denoland/deno 对“分发一个单独的可执行文件”这件事一直都很重视,所以deno compile 在这个话题里存在感很强。和 pkg、Bun 不同,Deno 的思路不是把 Node 的世界包装起来,而是直接站在 Deno 运行时 的语境里,把“可执行产物”当作默认能力的一部分。
优势:
- 单文件分发能力成熟
- 权限模型清晰
- 对现代 JS / TS 支持自然
特点:
- 它不是 Node,因此心智模型会有差异
- 本次最小测试里,产物明显比 Bun 更大
- 它更像“自成体系的打包分发方案”
我的判断:
如果不执着于 Node 运行时本身,Deno 是一个非常完整、气质也很统一的答案。
QuickJS
QuickJS 是另一个很有意思的方向,我之前也单独写过:quickJs
它和前面几种方案最大的区别是:它不是在讨论“如何把一个 Node 项目交付出去”,而是在讨论“如何把一段 JavaScript 放进一个非常轻的 JS 引擎里”。
qjsc 可以把脚本编译为可执行产物,但它的定位和 Node、Bun、Deno 都不一样。
优势:
- 很轻
- 启动快
- 在这个最小例子里,大小远小于其他方案
特点:
- 它不是 Node.js
- 没有 Node 的内建模块体系,也没有 npm 世界那套默认能力
- 它更像一个“小型 JS 引擎 + 编译器”,而不是通用工程分发方案
我的判断:
QuickJS 很强,但它强在“轻”和“干净”,不强在 Node 兼容性。把它和 pkg、Bun、Deno 放在一起比较时,最好把它看作另一条路线。
还可以补充什么
除了上面四个,还可以补几个经常会被提到的名字。
Node.js SEA
Node.js 官方提供了 SEA(Single Executable Applications)能力。它的重要性不在于“最省事”,而在于它代表的是 Node 官方对单可执行应用的正式路线。
特点:
- 官方能力
- 和 Node 语义最一致
- 这次最小测试里产物最大
我的判断:
如果一篇文章要完整讨论“把 JS 打包成可执行程序”,那 SEA 很值得出现,因为它不是社区绕出来的方案,而是 Node 自己在做这件事。
nexe
pkg 接近,同样属于“把 Node 和脚本封在一起”的路线。特点:
- 历史上使用很多
- 和
pkg一样,属于比较经典的 Node 打包方案 - 我这次在本机测试时,没有直接产出可用结果
对比下来怎么理解
如果把这些方案放在一起看,会很清楚地发现它们其实分成了三类。
第一类,是 Node 世界内部的打包方案。
代表是 pkg、Node.js SEA、nexe。它们的共同点是都尽量站在 Node 语义内部解决问题,因此讨论重点通常是兼容性、资源、依赖、运行时行为。
第二类,是自带运行时的一体化方案。
代表是 Bun 和 Deno。它们不是在“包装 Node”,而是在用自己的运行时,重新定义“JS 可执行程序”这件事。因此它们给人的感受往往更现代,也更统一。
第三类,是轻量引擎路线。
代表是 QuickJS。它讨论的不是工程项目分发,而是极轻量执行。
这样看下来,pkg、Bun、Deno、QuickJS 虽然都能让 JavaScript 变成可执行文件,但它们解决的其实不是完全同一个问题。
小结
只看这个最小的 hello, world,结论其实已经很清楚了:
- 要最小体积,QuickJS 优势巨大
- 要 Node 兼容思路,
pkg和 SEA 更接近原生 Node 世界 - 要现代体验,Bun 和 Deno 更像新一代方案
- 要官方路线,SEA 不能绕开,但体积并不占优
所以“将 JS 打包为可执行程序”这件事,真正要比较的不是有没有这个能力,而是你更在意哪一边:体积、兼容性、还是路线本身。