从 0.1 到 26:一个运行时的十七年进化史
2009 年 5 月 27 日一个叫 Ryan Dahl 的年轻人往 GitHub 上推了第一个 commit。那是一个用 C 写的 JavaScript 运行时把 Google 开源的 V8 引擎包了一层加了一个事件循环和一个底层 I/O 接口。初始版本号 0.1.0只跑在 Linux 和 macOS 上连 Windows 都不支持。半年后的 11 月 8 日Dahl 在柏林的 JSConf EU 上做了第一次公开演示。那场演讲结束时台下起了 standing ovation。一个在浏览器里被关了十几年的语言突然有了在服务端运行的可能。十七年后的 2026 年 5 月 5 日Node.js 发布了 v26.0.0代号 Lithium。V8 引擎升到了 14.6内置了 Temporal APIUndici 升到了 8.0。从一个人写的实验项目变成了全球服务端 JavaScript 的事实标准基础设施。这篇文章要做的事情是把这十七年间的关键变化拆开来看——不是罗列 changelog而是搞清楚每个版本到底改变了什么以及为什么要这样改。架构地基V8、libuv 和事件循环要看懂 Node.js 的版本变化先得理解它的底层架构。这不是一个从零开始的运行时而是三个东西的组合体。V8 是 Google 写的 JavaScript 引擎负责把 JS 代码编译成机器码执行。Node.js 从第一天起就用 V8这是整个项目能存在的技术前提——2008 年 V8 以 BSD 协议开源Dahl 才有了搭建 Node.js 的基础。但 V8 本身没有事件循环没有文件系统访问没有网络能力它只是一个 JS 执行器。libuv 是 Node.js 的异步 I/O 层。最早 Node.js 用的是 libev 和 libeio但这两个库只支持 Unix 系统。2011 年 Microsoft 和 Joyent 合作做 Windows 原生支持时需要一个跨平台的异步 I/O 抽象层于是 libuv 被创造出来。libuv 的事件循环是 Node.js 异步模型的核心它还维护了一个线程池默认 4 个线程可通过 UV_THREADPOOL_SIZE 调整用来处理那些系统层面没法非阻塞的操作比如文件 I/O。第三个组件是一组核心模块——HTTP、TCP、UDP、DNS、TLS/SSL、文件系统、Buffer、Stream、Crypto 等。这些模块构成了 Node.js 的标准库让你不用装任何第三方包就能写出一个 HTTP 服务器。理解了这个架构就能理解 Node.js 十七年的变化本质上是在做什么不断升级 V8 获取新 JS 特性和性能提升不断完善 libuv 的异步能力不断扩展和重构核心模块以及不断调整模块系统和开发者工具来适应越来越大的生态。发布周期从随意到半年一次再到一年一次Node.js 的发布策略本身经历了三个阶段。2009 到 2014 年是随意发布阶段。版本号从 0.1 一路蹦到 0.12没有固定节奏什么时候准备好什么时候发。v0.6 在 2011 年发布第一次把 npm 打包进了 Node.js 发行版。v0.8 在 2012 年发布带来了完整的 Windows 原生支持。v0.10 在 2013 年 3 月发布成为了第一个被广泛生产使用的版本。v0.12 在 2015 年 2 月发布是 io.js 分叉前的最后一个版本。2015 年 v4.0 开始Node.js 采用了正式的发布周期每年两个大版本4 月发偶数版本10 月发奇数版本。偶数版本在 6 个月的 Current 阶段后升级为 LTS长期支持版本获得 12 个月活跃支持加 18 个月维护支持总共 30 个月的 LTS 生命周期。奇数版本只有 6 个月寿命用完即弃。这套规则的设计初衷是让企业有可预测的升级节奏同时给新特性一个实验场。2026 年 3 月Node.js 官方宣布从 v27 开始改为年度发布。版本号与年份对齐27.0.0 在 2027 年发布28.0.0 在 2028 年发布。取消奇偶区分每个版本都成为 LTS。原因是十年数据显示奇数版本采纳率极低大多数用户只升级到 LTS奇偶区分反而困扰新手。同时维护四五个活跃发布线的负担已经让志愿者团队不堪重负。v26 是旧发布周期的最后一个版本也是最后一个奇数月发布的版本。从 v27 开始Node.js 进入一年一个 LTS 的新纪元。io.js 分叉与合并Node.js 最危险的一年2014 年 12 月一个叫 Fedor Indutny 的核心开发者 fork 了 Node.js创建了 io.js。原因不是技术分歧而是治理不满。当时 Node.js 的版权持有方 Joyent 对项目的管理被社区认为过于保守——V8 已经发到 4.1 了Node.js 还在用老版本社区贡献的 PR 大量积压没人合。io.js 采取了开放治理模式有独立的技术委员会快速跟进 V8 最新版本吸引了一批核心贡献者过去。短短三个月内 io.js 就发布了 1.0、2.0、3.0 三个大版本。2015 年 2 月在 Linux Foundation 的斡旋下Node.js Foundation 成立。9 月io.js v3.3 和 Node.js v0.12 合并成了 Node.js v4.0代号 Argon。这次合并的意义远超版本号它不仅统一了社区还把 io.js 带来的 V8 4.5支持 ES6 特性和开放治理模式正式纳入了 Node.js 主线。LTS 发布周期也正是从 v4 开始的。2019 年JS Foundation 和 Node.js Foundation 合并成立了 OpenJS Foundation成为 Node.js 的新家。模块系统CommonJS 与 ESM 的十二年拉锯Node.js 的模块系统演进是整个版本历史中最纠结的一条线。CommonJS 是 Node.js 的原始模块系统。你写require(http)它同步加载模块返回 exports 对象。这套系统简单、直观、同步执行在 2009 年是 JavaScript 在浏览器外运行的事实标准那时候 ES6 连影子都没有。但 CommonJS 有设计上的硬伤它是同步的不利于静态分析不支持 tree-shaking模块导出的是值的拷贝而非引用。ES6 规范定义了官方的 ES ModulesESM标准用import和export语法支持静态分析和 live bindings。把 ESM 引入 Node.js 花了六年。v132019 年 10 月首次支持 ESM。你可以用 .mjs 扩展名或者在 package.json 里写type: module就能用import和export了。但每次加载都会打印实验性警告。v142020 年 4 月去掉了 ESM 的实验性警告ESM 正式成为可用状态。同时支持了 top-level await——在 ESM 模块顶层可以直接写await不用包 async IIFE。但问题来了CommonJS 和 ESM 之间有互操作性难题。CommonJS 用require()ESM 用import两者的模块解析算法完全不同。一个 CommonJS 模块想require()一个 ESM 模块不行因为require是同步的而 ESM 模块加载是异步的。v222024 年 4 月终于开始解决这个问题实验性支持require()同步加载 ESM 模块。只要 ESM 模块的依赖图是同步可解析的就可以用require()引入它。这个功能由 Joyee Cheung 实现她在博客里说这是a feature that has been long overdue。v232024 年 10 月把这个功能推广到了更多场景稳定性进一步提升。v252025 年 10 月默认开启了 TypeScript type stripping意味着 Node.js 原生支持 .ts 文件了——不需要 ts-node不需要 tsx直接node app.ts就能跑。类型注解会被剥离但不做类型检查你想做类型检查还得靠 tsc。到 v26require(esm)已经从实验走向稳定CommonJS 和 ESM 之间的墙终于被打通了。十二年的拉锯算是有了一个务实的结论两套模块系统共存互操作尽可能无缝。V8 引擎升级每个版本背后的性能引擎Node.js 的 JavaScript 能力直接取决于 V8 的版本。每次大版本升级都伴随着 V8 更新带来新的 JS 语法支持和性能改进。以下是主要版本与 V8 版本的对应关系Node.js 版本V8 版本带来的关键 JS 特性v4 (2015)V8 4.5ES6 基础特性let/const、箭头函数、class、Promise、模板字符串v6 (2016)V8 5.1ES6 完整支持性能优化v8 (2017)V8 5.8启动速度大幅提升Async/Await 支持v10 (2018)V8 6.6解析器优化内存占用降低v12 (2019)V8 7.4更快的 async/await更好的性能v14 (2020)V8 8.1可选链?.、空值合并??、Intl 改进v15 (2020)V8 8.6逻辑赋值运算符、v16 (2021)V8 9.0正则表达式 /d 标志RegExp 匹配索引v17 (2021)V8 9.5性能优化v18 (2022)V8 10.1Array.findLast()、Array.findLastIndex()v19 (2022)V8 10.7性能优化v20 (2023)V8 11.3数组变更方法toSorted()、toReversed() 等v21 (2023)V8 11.8Array.groupBy()v22 (2024)V8 12.4Array.fromAsync()Set 新方法v24 (2025)V8 13.6Float16ArrayRegExp.escape()v25 (2025)V8 14.1Uint8Array base64/hex 辅助方法v26 (2026)V8 14.6Map.getOrInsert()Iterator.concat()这张表解释了一件事你在 Node.js 里能用的新 JavaScript 语法不是 Node.js 自己发明的而是 V8 升级带来的。Node.js 的角色是决定何时将新版 V8 引入稳定版。通常 V8 跟随 Chrome 浏览器发布Node.js 团队会把它适配到自己的构建系统中。一个值得注意的细节v14 给 JavaScript 带来了可选链?.和空值合并??运算符这两个特性对日常编码影响巨大——你不用再写obj obj.prop obj.prop.value了直接obj?.prop?.value。如果你还在用 v12 或更低版本你享受不到这些语法糖。Streams从手写到对齐 Web 标准Stream 是 Node.js 最核心的概念之一。任何数据流——HTTP 请求体、文件读写、TCP 连接——底层都是 Stream。早期的 Stream APIv0.x 时期设计得比较粗糙。v0.10 引入了 Stream 2重写了流的背压backpressure机制。v0.12 引入了 Stream 3进一步改进了 push/pushback 逻辑。但 Stream API 一直有一个问题错误处理不完善管道泄漏时不会自动清理上游资源。v10 引入了stream.pipeline()解决了pipe()的错误处理和资源清理问题。你可以把多个流串起来任何一个流出错pipeline 会自动销毁所有流并调用回调。pipeline(readable, transform, writable, (err) { if (err) console.error(Pipeline failed, err) else console.log(Pipeline succeeded) })v182022 年是一个分水岭Node.js 引入了 Web Streams API与浏览器的ReadableStream、WritableStream、TransformStream完全对齐。这意味着你写的流处理代码可以在 Node.js 和浏览器之间无缝迁移。到 v26老的_stream_wrap、_stream_readable、_stream_writable等内部模块被完全移除Stream API 完成了从Node.js 私有设计到Web 标准对齐的转型。Worker Threads打破单线程的限制Node.js 的事件循环是单线程的这对 I/O 密集型场景非常高效但对 CPU 密集型任务是个灾难——一个长时间运行的同步计算会阻塞整个事件循环所有请求都卡住。早期的解决方案是 cluster 模块它允许多个 Node.js 进程共享同一个端口。但 cluster 本质上是多进程进程间通信靠 IPC数据不能直接共享内存。v102018 年引入了实验性的 Worker Threads 模块在旗标后面开启。它允许在 Node.js 进程内创建真正的工作线程共享SharedArrayBuffer内存。v112018 年 10 月去掉了旗标要求Worker Threads 成为不需要特殊标志就能使用的实验性功能。v122019 年 4 月将 Worker Threads 标记为 Stable。感谢 Anna Henningsen 的工作这个版本终于让 Node.js 有了处理 CPU 密集型任务的标准方式。const { Worker } require(worker_threads) const worker new Worker(./heavy-calc.js) worker.on(message, (result) console.log(result)) worker.postMessage({ data: start })Worker Threads 并不意味着 Node.js 变成了多线程运行时。主线程仍然是单线程事件循环Worker Threads 是补充手段适用于图像处理、加密计算、数据序列化等场景。N-API原生模块的 ABI 稳定性Node.js 的原生模块用 C/C 编写直接调用 Node.js 的内部 API。在 N-API 出现之前每次 Node.js 大版本升级原生模块都需要重新编译因为 Node.js 内部的 ABI应用二进制接口不稳定。这导致node-gyp rebuild成为升级 Node.js 版本时最痛苦的环节。v82017 年引入了实验性的 N-API提供了一套稳定的 C API让原生模块不再依赖 V8 的内部 API。这意味着用 N-API 写的模块在 V8 版本升级时不需要重新编译。v102018 年把 N-API 标记为 Stable版本号独立于 Node.js 版本号维护。你可以用 N-API 在 C、C 甚至 Rust 中编写原生模块一次编译到处运行。N-API 的意义在于它把 Node.js 从绑定 V8 内部 API升级为提供稳定 ABI 层让原生模块生态不再被 V8 升级牵着走。内置 Web API从 polyfill 到原生支持很长一段时间里Node.js 缺少浏览器里理所当然的 API。你要发 HTTP 请求装 node-fetch 或 axios。你要处理 URL用 url 模块而非 URL 构造器。你要 WebSocket装 ws 包。这造成了一个尴尬现实同一段 JavaScript 代码在浏览器和 Node.js 里跑不通因为 API 不一样。v17.52022 年初在旗标后面引入了实验性 fetch API由 undici 项目提供底层实现。undici 是 Node.js 团队自己写的 HTTP/1.1 客户端专为 Node.js 的事件循环设计比基于 node-fetch 的 polyfill 快得多。v182022 年 4 月把 fetch 设为默认可用的全局 API不需要任何标志。同时引入了Headers、FormData、Blob等关联 API以及BroadcastChannel用于跨进程消息广播。v212023 年 10 月在旗标后面引入了实验性原生 WebSocket 客户端以及全局navigator对象提供navigator.userAgent等浏览器惯用接口。v222024 年 4 月将 WebSocket 设为稳定可用的内置 API不再需要第三方包就能建立 WebSocket 连接。同时node:fs模块内置了glob和globSync函数不再需要装 glob 包。v252025 年 10 月引入了实验性 Web Storage APIlocalStorage/sessionStorage进一步缩小与浏览器 API 的差距。这些变化的战略意图很明确让 Node.js 尽可能兼容 Web 平台标准让同构代码isomorphic code成为现实而非奢望。测试运行器告别第三方测试框架直到 v18 之前Node.js 没有内置测试框架。你必须装 jest、mocha、tap 或 vitest。不是不行但一个运行时连测试都要靠第三方包总觉得差点意思。v182022 年 4 月引入了实验性node:test模块。不用装任何东西直接const test require(node:test) test(basic test, (t) { t.assert.strictEqual(1 1, 2) })v202023 年 4 月将测试运行器标记为 Stable并添加了 mock 功能、代码覆盖率支持、以及更丰富的断言 API。v222024 年进一步增强了测试运行器支持 glob 模式匹配测试文件、–watch 模式自动重跑。v242025 年让测试运行器自动等待子测试完成修复了一个长期困扰开发者的竞态条件。到 v26Node.js 内置测试运行器已经可以替代大多数中小型项目的测试框架需求。大型项目可能还需要 jest 或 vitest 的快照测试和 mock 强大能力但对单元测试和集成测试node:test足够了。权限模型运行时安全的第一步Node.js 一直有一个安全问题任何脚本都可以访问文件系统、网络、环境变量。你装了一个 npm 依赖它可能在 postinstall 脚本里干任何事——读你的 SSH key上传到远程服务器你都不会察觉。v202023 年 4 月引入了实验性权限模型。你可以用--allow-fs、--allow-env、allow-net等标志限制脚本的访问范围node --allow-fs-read./data --disallow-net app.js这个功能还在实验阶段API 可能会变。但它的方向是对的Node.js 终于开始像 Deno 那样提供运行时级别的权限控制而不是把安全责任全甩给操作系统。v25 进一步增加了--allow-net的细粒度控制可以指定允许访问的主机名白名单。TypeScript 原生支持Node.js 对 TypeScript 的态度经历了一个明显的转变。v222024 年 4 月引入了--experimental-transform-types标志允许 Node.js 直接执行 .ts 文件。但这个方案是用 TypeScript 编译器做完整类型转换启动速度慢依赖 tsx 或 ts-node 作为后备。v232024 年 10 月引入了--experimental-strip-types采取了一个更轻量的方案不做类型转换只剥离类型注解。类型检查依然需要 tsc但运行 .ts 文件不再需要任何编译步骤。v252025 年 10 月把 type stripping 设为默认行为。你直接node app.ts就能跑 TypeScript 文件不需要任何标志。这个功能由 Node.js TSC 成员 Marco Ippolito 贡献他同时也是 Babel 的维护者。v262026 年 5 月移除了--experimental-transform-types意味着完整类型转换路线被放弃Node.js 明确选择了只剥离类型不做类型检查的方案。你想做类型检查用 tsc。你想运行 TypeScript直接 node。这个决策背后的理念是运行时不应该承担类型检查的职责那是编译器的事。Node.js 只需要能执行 TypeScript 代码类型正确性由开发者在 CI 里用 tsc 验证。v26 的核心变化2026 年 5 月 5 日发布的 v26 代号 Lithium是旧发布周期的收官之作。几个值得关注的变化Temporal API 默认启用。Temporal 是 JavaScript 的新一代日期时间 API设计用来替代臭名昭著的Date对象。Date 有大量设计缺陷月份从 0 开始、时区处理混乱、没有日期范围类型。Temporal 提供了Temporal.Now、Temporal.PlainDate、Temporal.ZonedDateTime等类型彻底解决这些问题。由 Richard Lau 在 PR #61806 中贡献。V8 升级到 14.6.202.33对应 Chromium 146。新增了 Map.prototype.getOrInsert() 和 Iterator.concat() 等特性。getOrInsert 解决了一个常见模式从 Map 取值不存在时插入默认值以前要写三行代码现在一行。Undici 升级到 8.0带来了 HTTP 客户端的性能改进和新特性。一批旧 API 被移除http.Server.prototype.writeHeader()被完全删除用writeHead()替代旧版 Stream 内部模块_stream_wrap等被移除module.register()被运行时弃用。这些清理为未来的稳定维护扫清了道路。GCC 编译器要求提升到 13.2Python 3.9 不再被支持构建工具链现代化。版本代号从 Argon 到 Lithium从 v4 开始每个 LTS 版本都有一个元素代号。这个传统借鉴自 Ubuntu 的命名策略目的是让版本号更容易记忆和指代。v4 Argon氩v6 Boron硼v8 Carbon碳v10 Dubniumv12 Erbium铒v14 Fermium镄v16 Gallium镓v18 Hydrogen氢v20 Iron铁v22 Jodv24 Krypton氪v26 Lithium锂按元素周期表顺序排列但跳过了部分元素——没有 Boron 之后跳到 Carbon 是连续的从 Dubnium 开始每个代号的首字母按字母表顺序排列D-E-F-G-H-I-J-K-L这是有意设计的双重命名规则。Ryan Dahl 的反思与 Deno 的竞争2009 年柏林 JSConf 那场演讲九年后Ryan Dahl 在 2018 年的同一场地做了另一场演讲标题是 “10 Things I Regret About Node.js”。他列出了 Node.js 设计中的十个遗憾没有从第一天就引入 PromisePromise 在 v0.x 时代不存在后来加得很别扭安全模型缺失任何包都能访问一切构建系统依赖 GYP太复杂package.json 的设计让它变成了包信息的中心而非简单的依赖列表node_modules 的递归目录结构安装慢磁盘占用大require 默认加 .js 扩展名自己写了一套模块系统而不是直接用 CommonJS 规范基于这些反思Dahl 创建了 Deno——一个用 Rust 写的、内置 TypeScript 支持的、默认安全的、不支持 npm 的 JavaScript 运行时。2020 年 Deno 1.0 发布一度被视为 Node.js 的颠覆者。2024 年另一个竞争者 Bun 出现用 Zig 写的 JavaScript 运行时主打极致的启动速度和 npm 兼容性。面对竞争Node.js 的回应不是重建而是渐进式吸收——Deno 的安全模型启发了 Node.js v20 的权限模型Deno 和 Bun 的 TypeScript 原生支持推动了 Node.js v25 的 type stripping。Node.js 选择在自己的架构基础上吸收竞争者的优点而非推翻重来。2024 年 Dahl 本人在一次访谈中说写代码的时代结束了——他的关注点已经从运行时转向了 AI 编程。Node.js 则继续在自己的轨道上运转2026 年 v26 发布2027 年 v27 将开启年度发布的新周期。写在版本号的尽头从 v0.1 到 v26Node.js 走了十七年。这十七年的变化可以总结为几条主线模块系统从 CommonJS 一家独大到 ESM 加入后的十二年拉锯最终以require(esm)的互操作方案告一段落。异步模型从回调地狱到 Promise 被 V8 带入到 async/await 成为标配到 AsyncLocalStorage 提供上下文追踪到 top-level await 打破最后的限制。Web API 从缺失到补齐——fetch、WebSocket、Web Streams、AbortController、BroadcastChannel、URL Pattern——Node.js 从浏览器 API 的旁观者变成了 Web 平台标准的实现者。原生模块从绑死 V8 ABI到 N-API 提供稳定接口原生模块不再随 V8 升级而碎。安全模型从完全没有到 v20 引入实验性权限控制——这是 Node.js 二十年来最大的安全架构变化。TypeScript 从必须用 ts-node 编译到 v25 默认 type stripping 直接运行——Node.js 终于承认 TypeScript 已经是 JavaScript 生态的实际标准。发布周期从随意发布到半年一次的奇偶双轨到 v27 开始的年度发布——版本号将与年份对齐Node.js 用一种更从容的节奏面对未来。十七年前 Ryan Dahl 在那个 commit 里写的第一行代码到今天仍然是 Node.js 的地基。上面的建筑已经换了无数轮但 V8 引擎加事件循环加异步 I/O 的核心架构从未被推翻。一个好的架构大概就是这样——经得起十七年的扩展和重构而不需要推倒重来。

相关新闻