Node.js 26 深度拆解:当运行时进化成平台——从原生 TypeScript、Temporal 默认启用到 Permission 权限模型与内置 SQLite 的工程全貌(2026)
发布日期:2026-07-18 · 栏目:编程 · 关键词:Node.js 26、TypeScript 原生支持、Permission Model、Temporal、node:sqlite、require(esm)
如果你最近半年没碰过 Node.js,那么 2026 年的它大概率已经不是你印象里那个「装个 ts-node 才能跑 TS、配个 dotenv 才能读环境变量、引个第三方库才能连 SQLite」的运行时了。
Node.js 26.0(Current 线)于 2026 年上半年发布,按计划将在同年 10 月进入 LTS,搭载 V8 14.x。这一版最值得工程师关注的,不是某个单点性能数字,而是整个产品形态的迁移:Node.js 从一个「需要海量周边工具链才能好用的运行时」,正在变成一个「开箱即用的平台」。原生 TypeScript 执行、默认启用的 Temporal、稳定的权限模型、内置 SQLite、对 ESM 的 CJS 反向兼容——这些能力叠加起来,直接动摇了我们过去十年搭建的那套 tsc + ts-node + dotenv + better-sqlite3 + 各种 polyfill 的脚手架。
本文从工程视角,把 Node.js 26 的几块核心能力拆开讲透:它们解决了什么真实痛点、底层机制是什么、代码里怎么用、生产环境怎么迁移、以及和 Bun / Deno 的取舍。所有示例均在 Node.js 26 语境下可运行(标注「实验性」的部分除外)。
一、背景介绍:为什么 Node.js 26 是一个分水岭
回顾 Node.js 的演进,过去很长一段时间里它的主线是「补历史债」:
- 模块系统分裂:CJS 与 ESM 长期双轨,CJS 无法直接
importESM,导致大量库必须同时发布两种格式,Tree-shaking 受限。 - TypeScript 必须转译:Node 原生只认 JavaScript,跑
.ts必须依赖tsc/ts-node/tsx/esbuild,每一步都增加心智负担和构建时间。 - 日期时间一团糟:
Date对象时区语义混乱、不可变对象缺失、算术易出错,社区被迫长期依赖moment/dayjs/date-fns。 - 安全默认零信任缺失:Node 进程默认对文件系统、网络、子进程拥有完全访问权,运行不可信代码等于把机器完全交出去。
- 嵌入式存储要引第三方:想持久化点结构化数据,得装
better-sqlite3(带原生编译)或sqlite3。
Node.js 26 一次性把上面五条里的四条推到了「原生可用」:TypeScript 类型剥离稳定、Temporal 默认启用、Permission Model 稳定、node:sqlite 内置。这不是修修补补,而是把「平台能力」下沉到了运行时内核。
一点客观定位:Node.js 26 并不是要取代 Bun 或 Deno,三者定位不同。Bun 的卖点是一体化工具链与极致冷启动,Deno 的卖点是默认安全与 Web 标准亲和。Node.js 26 的卖点是 「在不破坏 2000 万 npm 包生态的前提下,把现代能力做成内核一等公民」——这恰恰是最难、也最容易被低估的一条路。
二、核心概念:五大一等公民能力拆解
2.1 原生 TypeScript 支持(Type Stripping)
Node.js 26 中,执行 TypeScript 不再需要任何转译器。核心机制是 Type Stripping(类型剥离):编译器在加载 .ts 文件时,只把类型注解「擦除」,产出纯 JavaScript,不做任何类型检查、不做任何类型驱动的代码生成。
底层由 Amaro 引擎承担(基于 SWC 的轻量剥离器演化而来)。它的哲学非常明确:运行时只负责「让 TS 能跑」,类型正确性交给 tsc / 编辑器去保证。这带来一个关键约束——
类型剥离是「语法擦除」,不是「语义转换」。凡是需要运行时生成代码的东西,剥离模式都不支持:
enum、namespace、参数属性(parameter properties)、装饰器的某些形态。需要这些特性的场景,要开--experimental-transform-types。
这意味着 Node 26 的 TS 支持和 tsc 是互补而非替代:开发期用 node 直接跑,发布前用 tsc --noEmit 做类型门禁。两者职责清晰。
# 直接跑 TS(26 中类型剥离已默认对 .ts 生效,下面两种写法等价)
node server.ts
node --experimental-strip-types server.ts # 老版本兼容写法
# 需要 enum / namespace 等语义转换时
node --experimental-transform-types server.ts
一个能直接跑的 HTTP 服务示例,注意里面既有 interface 类型,又用到了 Temporal(见 2.2):
// server.ts —— 无需编译,node server.ts 直接启动
import { createServer } from 'node:http';
interface User {
id: number;
name: string;
createdAt: string;
}
const users: User[] = [
{ id: 1, name: 'alice', createdAt: Temporal.Now.instant().toString() },
];
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
users,
serverTime: Temporal.Now.zonedDateTimeISO('Asia/Shanghai').toString(),
}));
});
server.listen(3000, () => {
console.log('Node 26 + TypeScript 原生运行,监听 3000');
});
迁移要点:如果你的项目之前靠 ts-node / tsx 跑脚本,现在可以逐步切到原生执行。但要记住——剥离模式不会读 tsconfig.json,也不会做 path alias 解析,所以 paths 映射、装饰器元数据这类需求仍需保留 tsc 或 tsx 在特定环节。
2.2 Temporal API 默认启用
Date 是 JavaScript 历史上最被诟病的 API 之一:可变对象、getFullYear() 这类反直觉命名、时区处理模糊、缺乏时长(Duration)与区间(Range)的一等抽象。Temporal 是 TC39 的 Stage 4 提案,Node.js 26 随 V8 14.x 默认启用,不再需要 --experimental 开关。
Temporal 提供了 PlainDate、PlainTime、ZonedDateTime、Duration、Instant 等不可变类型,时区显式、算术安全:
// 显式时区,不再有「这台机器时区是哪」的玄学问题
const now = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
console.log(now.toString());
// 2026-07-18T14:32:05+08:00[Asia/Shanghai]
// 安全的时长算术:不会因为夏令时跳变而出错
const deadline = now.add({ days: 14, hours: 9 });
const duration = now.until(deadline);
console.log(duration.toString()); // PT... 或 P14DT9H 形式
// 跨时区比较:Instant 是绝对时间锚点
const utc = Temporal.Now.instant();
const ny = utc.toZonedDateTimeISO('America/New_York');
const sh = utc.toZonedDateTimeISO('Asia/Shanghai');
console.log(ny.hour, sh.hour); // 同一瞬间,两个时区的小时数
// 业务常踩的坑:月末加减。Temporal 自动处理溢出
const endOfMonth = Temporal.PlainDate.from('2026-01-31').add({ months: 1 });
console.log(endOfMonth.toString()); // 2026-02-28(而非抛错或错位)
工程价值:凡是涉及账单周期、订阅续费、跨时区排程、SLA 计时的系统,Temporal 几乎能消灭一大类由 Date 引发的线上故障。建议新代码全面迁移,老代码用 dayjs 等库做兼容层即可。
2.3 Permission Model(权限模型)稳定
Node 进程的「全权」本质是历史遗留的安全债。Node.js 26 中,权限模型从 --experimental-permission 毕业为稳定的 --permission,开启后进程默认拒绝一切敏感操作,必须显式授权。
# 只允许读 /var/data、写 /var/data、允许子进程、允许网络
node --permission \
--allow-fs-read=/var/data \
--allow-fs-write=/var/data \
--allow-child-process \
--allow-worker \
app.js
也可以在代码里做程序化判断,实现「能力探测」:
// process.permission 在 --permission 开启时可用
import { permission } from 'node:process';
if (permission.has('fs.read', '/var/data')) {
// 授权路径
}
// 未授权操作会抛出 ERR_ACCESS_DENIED
import { readFileSync } from 'node:fs';
try {
readFileSync('/etc/shadow'); // 未授权
} catch (err) {
console.error(err.code); // 'ERR_ACCESS_DENIED'
}
实战场景:运行用户提交的插件、执行 CI 中不可信的第三方脚本、把 Node 当作沙箱跑多租户函数——过去你得靠容器隔离来兜底,现在进程内就能先挡一层。注意:权限模型是纵深防御的一层,不是容器/VM 的替代品;运行真正不可信代码仍要配合隔离。
2.4 node:sqlite 内置轻量数据库
node:sqlite 在 Node 22.5 以实验形态出现,到 Node 26 已成为稳定模块,无需装 better-sqlite3、无需原生编译,开箱即用同步 API(基于 SQLite 官方库):
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:'); // 或传文件路径做持久化
db.exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
);
`);
// 预编译语句,避免 SQL 注入、复用执行计划
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
insert.run('alice', 'alice@example.com');
insert.run('bob', 'bob@example.com');
// 查询
const select = db.prepare('SELECT id, name FROM users WHERE name LIKE ?');
console.log(select.all('a%')); // [ { id: 1, name: 'alice' } ]
// 事务:要么全成,要么全败
const tx = db.transaction((items) => {
for (const it of items) insert.run(it.name, it.email);
});
tx([{ name: 'c', email: 'c@x.com' }, { name: 'd', email: 'd@x.com' }]);
db.close();
价值:CLI 工具、本地缓存、嵌入式分析、原型验证——再也不用为「就存点结构化数据」去引一个带原生编译的包。需要高并发写或网络访问时,再上 PostgreSQL / SQLite 的 WAL + 连接池方案。
2.5 require(esm) 与模块系统收口
Node 22 引入、Node 26 已稳定的 require(esm):CommonJS 代码现在可以直接 require 一个 ESM 模块,且能正确拿到其 default 与具名导出。这彻底解开了「CJS 库无法消费 ESM-only 依赖」的死结。
// legacy.cjs —— 老 CJS 代码
const { parse } = require('some-esm-only-pkg'); // 过去会报错,现在 OK
const pkg = require('another-esm-pkg');
console.log(pkg.default, pkg.namedExport);
配合 Node 26 对 module/typescript 的识别(.ts 走类型剥离),模块生态终于收敛到「ESM 为主、CJS 兼容无感」的健康状态。
三、架构分析:这些能力是怎么落地的
3.1 类型剥离为何「快且安全」
关键认知:Type Stripping 不解析类型语义,只做语法层面的 token 擦除。它不会为 interface 生成任何运行时代码,也不会对 as 断言做运行时检查。对比 esbuild 的 transform,Amaro 的剥离更「窄」——它刻意不处理 enum / namespace,正是为了保证「擦除出的 JS 与原 TS 的运行时语义 100% 一致」。
代价与边界:
- 剥离阶段不读
tsconfig.json,所以paths、实验性语法配置无效。 - 类型错误不会被捕获(那是
tsc的职责)。Node 26 团队反复强调:运行时负责「能跑」,类型正确性由构建期的类型检查兜底。 - 需要
enum等语义转换时,必须用--experimental-transform-types,此时才引入轻量转换逻辑。
这套「分层」设计是 Node 26 TS 支持能稳定下来的根本原因:不试图在运行时做编译器该做的事。
3.2 Permission Model 的实现骨架
权限模型本质上是 Node 内部所有敏感操作在落地前的一次「闸门检查」。以文件操作为例:fs 模块在执行 open / read / write 前,会先向权限子系统查询当前进程是否被允许访问该路径;未授权则抛出 ERR_ACCESS_DENIED,且不会先执行再回滚,而是前置拒绝。路径匹配支持前缀(如 /var/data 覆盖其下所有子路径),也支持通配。
它的设计哲学是「默认拒绝 + 显式授权」,这与 Deno 的 --allow-* 一脉相承,但 Node 的优势在于:你可以在不改一行业务代码的前提下,给既有 CJS/ESM 老项目套上沙箱——只要启动参数加上 --permission 与对应的 --allow-*。
3.3 V8 14.x 带来的底层红利
Node 26 搭载的 V8 14.x 不只是 Temporal 默认化,还包括:
- 更快的解析与编译(冷启动收益);
- 持续优化的 GC 停顿;
- 更稳的 ECMAScript 2026 特性支持(如更完善的不可变数据结构、资源管理等)。
这些红利对「函数即服务 / 边缘计算」这类冷启动敏感场景尤其友好。
四、代码实战:把 Node 26 能力串成一条生产链路
下面用一个「本地任务看板」的小项目,把前面所有能力真实串起来,演示从开发到发布的可运行形态。
4.1 项目结构(无需构建步骤)
board/
├── package.json # {"type":"module"} 或保持 CJS 均可
├── board.ts # 主程序:TS 原生运行
└── seed.cjs # 老 CJS 脚本:require(esm) 演示
4.2 主程序:TS + SQLite + Temporal + 权限
// board.ts
import { DatabaseSync } from 'node:sqlite';
import { createServer } from 'node:http';
interface Task {
id: number;
title: string;
due: string; // Temporal 格式字符串
done: number; // 0 | 1
}
const db = new DatabaseSync('./board.db');
db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
due TEXT,
done INTEGER DEFAULT 0
);
`);
const addTask = db.prepare('INSERT INTO tasks (title, due) VALUES (?, ?)');
const listTasks = db.prepare('SELECT * FROM tasks ORDER BY id DESC');
function add(title: string, inDays: number) {
const due = Temporal.Now.zonedDateTimeISO('Asia/Shanghai')
.add({ days: inDays })
.toString();
addTask.run(title, due);
}
function list(): Task[] {
return listTasks.all() as Task[];
}
// 启动参数注入:node --permission --allow-fs-write=. board.ts add "写周报" 3
const [,, cmd, ...rest] = process.argv;
if (cmd === 'add') {
add(rest[0], Number(rest[1] ?? 7));
console.log('已添加,截止:', list()[0].due);
} else {
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(list()));
});
server.listen(3000, () => console.log('看板 API: http://localhost:3000'));
}
跑起来:
# 写任务(注意权限模型:只允许写当前目录)
node --permission --allow-fs-write=. board.ts add "发布 Node26 文章" 2
# 起 API
node --permission --allow-fs-read=. board.ts
curl localhost:3000 # 返回 JSON 任务列表
4.3 老 CJS 脚本反向消费 ESM(require(esm))
// seed.cjs —— 传统 CommonJS,无需任何改造即可 require ESM
const { add } = require('./board-lib.mjs'); // 假设 board 抽出为 ESM 库
add('从 CJS 来的任务', 5);
console.log('seeded from CJS');
这一行在 Node 22 之前会直接报错;Node 26 下完全透明。对维护着几十万行 CJS 存量代码的企业来说,这意味着升级 Node 版本不必同时做模块系统大迁徙。
4.4 单文件可执行(SEA,实验性)
Node 26 的 node:sea 让你把应用打包成单个无依赖二进制,适合分发 CLI 工具:
// sea-config.json
{
"main": "board.ts",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true
}
node --experimental-sea-config sea-config.json
cp $(command -v node) board-bin
npx postject board-bin NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
./board-bin # 直接运行,无需 node 环境
注:SEA 在 Node 26 仍带实验性标记,API 细节可能变动,生产分发前请核对官方文档。
五、性能优化:在 Node 26 上写出更快的代码
5.1 冷启动:少一层转译就少一份开销
原生 TS 执行省掉了 ts-node / esbuild-register 的启动期转译成本。在「短生命周期进程」(CLI、Serverless、测试)场景下,这能直接削减数百毫秒启动耗时。评测方式:
time node --permission board.ts add "x" 1 # 原生
time ts-node board.ts add "x" 1 # 旧链路(对照)
经验值:简单 TS 脚本的启动在原生模式下通常比 ts-node 快一个数量级(具体数值随项目规模而异,请以你自身基准测试为准)。
5.2 数据库:预编译语句与 WAL
node:sqlite 走同步 API,瓶颈在「每条 SQL 都重新解析」。务必用 prepare 复用:
const insert = db.prepare('INSERT INTO t (a) VALUES (?)');
for (let i = 0; i < 10000; i++) insert.run(i); // 复用执行计划
// 高并发写开启 WAL
db.exec('PRAGMA journal_mode = WAL;');
5.3 权限模型的性能账
开启 --permission 后,每次敏感操作多了一次权限查询。对绝大多数 I/O 密集业务,这个开销远小于一次磁盘/网络往返,可忽略;但如果是极高频、极短路径的循环内文件操作,建议把授权路径设得足够宽、或把热点逻辑移出沙箱边界。一句话:安全默认值的收益 >> 那点查询开销。
5.4 与 Bun / Deno 的取舍(客观对照)
| 维度 | Node.js 26 | Bun | Deno |
|---|---|---|---|
| 生态兼容 | 最强(2000 万 npm 包) | 高(兼容大部分) | 中(Web 标准优先) |
| 冷启动 | 优(原生 TS) | 极优(Zig 内核) | 优(Rust + V8) |
| 默认安全 | 可选 --permission | 默认放开 | 默认沙箱 |
| 内置存储 | node:sqlite | 内置 SQLite | 无内置 |
| 工具链一体化 | 仍偏分散 | 最强(run/build/test/install 一体) | 较强 |
建议:存量大型项目、强依赖 npm 生态 → Node 26;追求极致冷启动与一体化 DX 的新项目 → Bun;强调默认安全与 Web 标准纯净 → Deno。三者 2026 年都已能打,选型看团队基因而非「谁更快」。
六、总结与展望
Node.js 26 的意义,不在于某个单点指标,而在于它完成了一次产品形态的心智迁移:把过去十年我们「默认应该自己搭」的能力,下沉成了运行时内核的一等公民。
- 开发体验:
node xxx.ts直接跑 TS、Temporal 让日期逻辑不再靠玄学、node:sqlite 让「存点数据」零依赖。 - 安全范式:
--permission让「跑不可信代码」从「必须上容器」降级为「进程内先挡一层」。 - 生态收敛:
require(esm)抹平了 CJS/ESM 的最后一道鸿沟,存量代码升级 Node 不再意味着模块系统大改。
迁移路线建议:
- 新项目直接上 Node 26 LTS(10 月后),TS 用原生执行 +
tsc --noEmit做类型门禁。 - 老项目先升级运行时、验证
require(esm)兼容性,再逐步把ts-node脚本切到原生执行。 - 涉及不可信代码执行的服务,补上
--permission+ 精确--allow-*授权。 - CLI / 嵌入式工具优先考虑
node:sqlite替代第三方 SQLite 包,减少原生编译与供应链面。
展望:Node.js 团队的方向很清晰——在不破坏生态的前提下,把现代语言与平台能力做成默认。当「运行时」进化为「平台」,我们写 Node 代码的姿势,从这一版开始真的要更新了。
本文基于 Node.js 26(Current,2026 年发布,计划同年 10 月进入 LTS)公开特性梳理。实验性能力(如 SEA、FFI)API 可能随版本调整,落地前请以官方 release notes 与文档为准。