JavaScript 运行时横向比较:按约束选择宿主
核验日期:2026-07-16。本文比较 Node.js 24 LTS、Deno 2.9、Bun 1.3 与 Cloudflare Workers 当前运行时。版本与平台限制只在该日期成立;选型时先重跑项目契约,再核对官方资料。
运行时选型不是引擎跑分投票。先写硬约束:必须使用的包/原生扩展、文件/进程、部署位置、数据一致性、权限、可观测性、LTS、预算与团队能力;淘汰不满足者;再对剩余候选比较可靠性、性能、使用难度、维护成本和适用规模。
四类宿主的核心模型
| 维度 | Node.js | Deno | Bun | Cloudflare Workers |
|---|---|---|---|---|
| 引擎 | V8 | V8 | JavaScriptCore | V8 Isolate |
| 执行单位 | OS 进程,可加 worker/子进程 | OS 进程,可加 Web Worker/子进程 | OS 进程,可加 worker/子进程 | 平台进程内 Isolate + invocation |
| 首要 API | Node APIs,逐步吸收 Web APIs | Web APIs + Deno APIs + Node compat | Web APIs + Bun APIs + Node compat | Web APIs + bindings + 部分 Node compat |
| 默认权限 | OS 用户能力;可启用 Node Permission Model | 敏感 I/O 默认拒绝,显式 allow/deny | OS 用户能力 | 平台只暴露允许 API/bindings,受配额 |
| 模块/包 | ESM+CJS、npm、node_modules、exports | ESM、JSR/npm/Node、cache/node_modules | ESM+CJS、npm、Node resolution/auto-install | ESM bundle、npm 经构建/compat、compat date |
| 工具链 | 核心 runtime/test;生态组合 package/build/lint | fmt/lint/check/test/task/compile 内置 | install/test/build/run 等一体化 | Wrangler + 平台部署、日志、bindings |
| 生命周期 | 长进程、signal、显式监听/关闭 | 长进程或平台化部署 | 长进程、显式监听/关闭 | 事件驱动、实例复用/驱逐、无进程所有权 |
| 状态 | 进程内临时;持久化外置 | 同左 | 同左 | isolate global 仅临时;平台存储/binding |
同样使用 V8 的 Node、Deno、Workers 也不是同一 runtime;同样能 import npm 的 Deno/Bun/Workers 也不是完整 Node。宿主、loader、I/O、权限和生命周期才是工程差异。
硬约束筛选
| 硬需求 | Node.js | Deno | Bun | Workers |
|---|---|---|---|---|
| 大量成熟 Node 包/APM/ORM | 成熟度最高,仍需版本测试 | 兼容性快速提高,需要组合测试 | 兼容目标明确,需要组合测试 | 只适合可 bundle 且无不支持宿主能力的子集 |
| V8/Node 内部或 legacy addon | 最可能满足 | Node-API/FFI 条件化 | Node-API 条件化,JSC 非 V8 | 通常不适合 |
| 任意文件、shell、daemon | 支持,需 OS 安全 | 显式权限支持 | 支持,需 OS 安全 | 不适合 |
| 默认最小 I/O 权限 | 可启用但需配置 | 原生默认 | 非默认 | 平台 capability/binding 模型 |
| 单 binary 全工具链 | 需组合或 SEA 等方案 | 强 | 强 | CLI + 云平台,不是本地通用进程 |
| 全球入口与轻量逻辑 | 需多区域部署 | 取决于部署平台 | 取决于部署平台 | 原生优势 |
| 强进程控制/heap 诊断 | 成熟 | 具备但生态不同 | 能力演进中 | 平台抽象,不拥有进程 |
| 长 CPU/大内存 | 容器配额内可设计 worker | 同类进程模型 | 同类进程模型 | 受 isolate/CPU 配额,通常外置 |
硬约束决定候选,不要用性能抵消缺失能力。例如必须运行依赖 V8 snapshot 的 agent 时,Bun 的 JSC 吞吐再好也不满足;必须全球低延迟执行轻量认证时,一个单区域 Node 进程生态再成熟也没有完成放置需求。
模块与兼容性
兼容验证分五层:解析安装、模块格式、API 行为、native/lifecycle、框架与诊断。建议维护同一 fixture:ESM/CJS、conditional exports、top-level await、JSON/Wasm、streams、crypto、worker、实际关键包。
Node 是 Node 行为参照,但 major 之间也变化;Deno/Bun 都在 minor/patch 中显著改善兼容;Workers 的 Node API 还受 Wrangler、native/polyfill/stub 与 compatibility date 影响。声明“支持 Node”时附精确 runtime 版本和通过的功能组合。
库作者优先标准 ESM、node: 显式宿主入口、清楚 exports,把文件/进程/平台 binding 放薄 adapter。不要发布四套表面相同而未测试的 conditions。需要 CJS 时保留真实 CJS artifact 和互操作测试。
权限与安全
Deno 默认拒绝最清晰;Node Permission Model 可把普通 Node 进程收紧;Bun 主要依赖 OS/container;Workers 由平台 API 和 bindings 限定能力。四者都不能仅靠 runtime 开关安全执行任意多租户代码。
| 风险 | 共同控制 | 差异 |
|---|---|---|
| 路径/secret | 固定 root、最小 secret、日志脱敏 | Deno/Node permission 可额外限制;Workers 通常无任意 fs |
| SSRF | URL allowlist、重定向复验、网络 policy | runtime 有 fetch 不代表浏览器 CORS 保护 |
| 子进程/native | allowlist executable、审计 addon、容器隔离 | Workers 通常不可用;Deno 需显式 allow |
| 供应链 | lock、frozen install、脚本策略、来源 | Deno 默认不跑 npm scripts;Bun/Node 策略需配置 |
| DoS | body/并发/CPU/内存/队列上限 | Workers 平台强制配额;进程 runtime 由部署层配额 |
默认拒绝能缩小 blast radius,但如果最终命令长期 -A 就失去主要收益。Workers isolate 隔离租户不表示在单 Worker 内 eval 用户代码安全。Node/Bun 的容器也不能替代应用授权。
调度、并行与 I/O
四者都运行单个 JavaScript event loop/realm,await 不会并行同步 CPU。Node 的 libuv 路径最容易观察线程池与阶段;Deno/Bun 的宿主实现不同;Workers 同 isolate 可并发推进多个 invocation,CPU 阻塞会影响共用实例。
CPU 任务在进程 runtime 使用有界 worker pool;Workers 通常拆到区域服务、Durable/Queue/专用计算。I/O fan-out 全部要有共同 deadline、有界连接和取消。streaming 全部需要慢消费者测试,尤其 Workers 128 MB 限制与 compatibility layer 的隐式 buffering。
生命周期与状态
Node/Deno/Bun 服务拥有进程生命周期:监听、readiness、SIGTERM、in-flight、连接池和 exit code。Workers 只拥有 invocation 与有限 waitUntil;平台决定 isolate 复用/驱逐。三种进程 runtime 的内存 Map 也不是持久化,但比 Edge 更容易产生“看起来一直在”的错觉。
权威状态都应外置。Workers isolate global 只做可丢失缓存;关键后台任务先持久入队。进程 runtime 收到信号时有界 drain;所有写操作用幂等键,因为响应丢失和重试与 runtime 无关。
性能比较协议
先通过相同正确性契约,再测:
- 固定硬件/区域、arch、runtime patch、依赖锁、日志和资源限制。
- 分冷启动、热启动、模块加载、稳态 HTTP、CPU、stream、install。
- 固定连接复用、协议、body、并发、预热、采样时长。
- 报告 p50/p95/p99、错误率、CPU、RSS/heap/平台内存、吞吐与成本。
- Workers 多地区区分 cache hit/miss 与 origin distance;本地 runtime 区分镜像拉取和进程启动。
- 保留原始 samples/命令,说明 JIT、GC、网络、转译和 type check 是否包含。
Node、Deno、Bun 厂商数据可用于设计复现实验,不能直接横比,因为版本、实现、handler 和环境不同。Edge 的收益主要是 placement,不应与本机 loopback RPS 放一张无上下文榜单。
可靠性、难度、维护与规模
| 候选 | 可靠性证据 | 使用难度 | 长期维护 | 合理规模 |
|---|---|---|---|---|
| Node | LTS、成熟生态、诊断与运维经验 | 需要组合工具和安全默认 | 依赖多但人才/平台广 | CLI 到大型服务;多区域需部署 |
| Deno | 一体化工具、默认权限、Node 兼容持续测试 | 新 API/权限/依赖模型需学习 | 工具更集中,兼容变化需跟踪 | 安全敏感工具、API、现代 TS 项目 |
| Bun | 单 binary、快速工具、Node test suite/实际框架 | 迁移简单路径低,边缘差异需查 | 快速迭代,patch/诊断需要细致测试 | CLI、测试、服务;关键生态需验证 |
| Workers | 托管全球网络、强平台限制/metrics | 无服务器运维但分布式/配额复杂 | vendor API、compat date、费用/绑定 | 全球入口、缓存、轻计算;重任务外置 |
“维护成本低”不是文件数量少。Deno/Bun 减少工具拼装,却可能增加组织新平台学习;Workers 不管理服务器,却要管理 bindings、配额、compat date 和分布式数据;Node 配置多,但现有团队可能已有成熟模板。成本按组织实际计算。
场景决策
成熟 Node 后端
默认留 Node LTS。若安装/测试耗时是明确痛点,可先单独试 Bun package manager/test;若需要最小权限,试 Node Permission Model 或 Deno compatibility lane。只有关键包、APM、native、shutdown 与 p99 都通过才切 runtime。
新的 TypeScript 内部工具
Deno 与 Bun 都有价值。需要默认拒绝、远程/JSR、内置检查时偏 Deno;需要 Node drop-in、一体化 package/test/build 和启动时偏 Bun;组织已有 Node 模板且无痛点时 Node 仍是最低变更。
全球请求前置层
Workers 适合 redirect、认证初筛、cache、route 和轻聚合。数据仍在单一区域且逻辑复杂时,让 Edge 只做前置,把事务留在数据库附近。用 trace 证明少了 RTT/源站负载。
重 CPU 或 native
优先 Node/Deno/Bun 进程 + worker/容器,按依赖/诊断选;Workers 只做路由/入队。若 Bun JSC 对计算有优势,测完整 worker/message/GC,不只测循环。
迁移与回滚
一次只改 package manager、test runner、bundler、runtime 或部署平台中的一层。锁住旧命令/镜像/lock/compat date。退出门槛应预先写:关键 API 不兼容、错误/尾延迟/内存恶化、诊断缺失、团队维护成本超预算即回退。
决策记录模板
一页即可:业务任务与 SLO;信任/数据边界;必须 API/包/native;候选和硬淘汰;正确性检查;同条件性能/资源/成本;部署生命周期与恢复;团队/生态维护;选定项、不选理由、复审触发器和回滚步骤。复审触发器包括 runtime major/minor、关键 package、数据位置、多租户、流量级别、平台配额/价格、诊断要求改变。
选型工作坊:把争论变成可证伪假设
团队可用半天完成第一次筛选。会前由业务负责人写三项不可谈判约束,例如“必须运行现有 native driver”“中国与欧洲数据不得跨指定区域”“SIGTERM 十秒内完成且能取得 heap/CPU profile”。平台负责人补充运行位置、配额、支持版本和成本;开发负责人列实际依赖与宿主 API;安全负责人列 secret、网络、子进程、不可信输入和供应链边界。
会上先执行硬淘汰,不讨论偏好。Workers 不支持必需 native/进程则不进入该服务候选,但可保留为前置层;Bun/Deno 对关键 APM 或 addon 未通过则暂缓,而不是用总兼容率投票;Node 单区域不满足全球入口时,考虑多区域部署或 Edge+Node 组合。淘汰理由写成可复审条件,例如“当 package X 在 Bun 精确版本通过 fixture 后重开”,不要写“生态不成熟”这种不可验证判断。
剩余候选每个提出一条可证伪假设:“Deno 最小权限能把服务读范围限制在单文件且全部测试通过”“Bun 在保持 APM/stream/shutdown 正确时将 CI 或启动降低到目标”“Workers cache miss 仍比区域服务 p95 低且不超过 subrequest 预算”。为假设指定实验、成功阈值、最长投入和退出门槛。没有阈值的 spike 容易无限延伸。
实验后把未知项作为风险,不把缺少证据自动记为通过。关键诊断能力未知可视为上线阻塞;低概率非关键 API 可记录规避。最终选择应包含组合架构的固定成本:Edge+区域服务增加部署、trace 和失败模式;Node+Deno 双 runtime 增加 CI/支持范围;Bun-specific API 增加迁移出口。组合只有在边界清楚且收益可测时采用。
评分只用于剩余候选
如果组织需要加权表,只能在硬约束筛选后使用。为可靠性、性能、开发体验、运维诊断、安全、生态、成本和迁移分别给权重;每个分数附证据链接和置信度。高权重但低置信度的项优先实验。不要把 4.2 与 4.1 当精确科学,分数的价值是暴露假设。
可靠性至少看错误契约、升级、恢复与支持窗口;性能看代表负载的 p99/资源/成本;开发体验看真实项目的 install/type/test/debug;运维看部署、profile、日志、告警、shutdown/eviction;安全看默认能力与纵深;生态看必需包而非 registry 总数;迁移看可分阶段和回滚。评分表每次 runtime/业务变化都可能过期。
组合架构的边界检查
常见合理组合是 Workers 负责公网入口/cache/auth 初筛,Node/Deno/Bun 在数据附近负责事务;或 Bun 只负责 package/test,生产仍 Node;或 Deno 运行受限内部工具,核心后端保持现状。组合边界使用标准 HTTP/queue/object store,不共享进程假设。trace context、request id、deadline 与幂等键必须跨边界传播。
组合也会失败:Edge 重试加源站重试会形成乘法风暴;两个缓存层 TTL/purge 不一致会返回旧版本;外层 timeout 短于内层但没有取消会留下幽灵工作;Edge 认证与源站授权规则漂移会绕过安全。设计时画出每层的 timeout、retry、cache、auth 和 owner,保证只有一层拥有最终业务决定。
总拥有成本
开发成本包括迁移、类型、测试、调试和培训;运行成本包括 CPU/内存/请求/带宽/日志/数据跨区;可靠性成本包括 on-call、升级、回滚、兼容回归和 vendor incident;机会成本包括专属 API 锁定和关键人才稀缺。单 binary 或更高 RPS 只影响其中一部分。
小团队若已有稳定 Node 平台,保持 Node 可能比引入新 runtime 便宜;大量短命任务可能因 Bun/Deno 工具集中明显受益;安全敏感 CLI 可能为 Deno 默认拒绝支付学习成本;全球轻逻辑可能为 Workers 托管能力支付平台费用。所有结论都必须带当前规模和团队背景。