Node.js:从 V8、libuv 到可运维进程
核验日期:2026-07-16。本文以 Node.js 24 LTS 为主要讲解版本,同时观察 22 LTS 与 26 Current。官方发布页显示 v24、v22 仍受支持,v26 处于 Current,v20 已结束生命周期;部署前应重新核对 Node.js Releases。
Node.js 的核心价值不只是“在服务器运行 JavaScript”,而是把 V8、模块加载器、libuv、系统库和一套长期兼容 API 组合成进程型宿主。理解它要沿真实数据流分层:CLI 先建立进程和 V8 Isolate,模块加载器把入口及依赖变成模块记录,JavaScript 在主线程执行,网络与文件工作进入不同的宿主路径,完成通知回到事件循环,最后由句柄引用、信号和退出码决定进程生命周期。
知识覆盖与学习结果
| 主题 | 必须能解释 | 必须能观察 |
|---|---|---|
| 引擎与宿主 | V8 执行 ECMAScript,Node/libuv 暴露 I/O;V8 版本不是 Node API 版本 | process.versions、CPU/heap profile、运行时精确版本 |
| 启动 | CLI 参数、权限、预加载、入口识别、模块求值的先后 | --trace-*、首个错误、cwd、NODE_OPTIONS |
| 模块 | ESM/CJS 的识别、解析、缓存、条件导出和互操作 | import.meta.resolve、错误码、--trace-require-module |
| 调度 | event loop phases、microtask、process.nextTick、timer 与 I/O | monitorEventLoopDelay、ELU、async stack |
| I/O | socket 通常由事件通知,部分文件/DNS/crypto 进入线程池 | 活跃句柄、线程池拥塞、stream backpressure |
| 并行 | worker thread、child process、cluster 的隔离与通信成本 | CPU profile、消息复制/transfer、退出和重启 |
| 生命周期 | ref/unref、SIGTERM、在途请求、优雅关闭、未处理错误 | shutdown 时间线、exit code、diagnostic report |
| 安全 | 输入验证、secret、路径、SSRF、子进程、addon 与权限模型 | ERR_ACCESS_DENIED、allow/deny 范围、审计记录 |
读完后应能解释“Node 单线程”为什么既正确又容易误导,能定位一次 event-loop lag,能设计 ESM/CJS 包边界,能在 SIGTERM 下停止接收请求并等待在途工作,且不会把 Node Permission Model 描述成完整沙箱。
运行时分层与一次请求
V8 管理 JavaScript heap、函数编译和垃圾回收;它不知道 node:fs 的业务语义。Node 的 JavaScript/C++ binding 把 API 调用转换为宿主操作;libuv 抽象不同操作系统的事件通知、线程池、定时器和进程信号。并非所有异步 API 都“在线程池运行”:网络 socket 通常依赖 epoll/kqueue/IOCP 等就绪或完成通知;fs、getaddrinfo、部分 crypto/zlib 才使用共享线程池。诊断时必须先确认具体 API 路径。
一次 HTTP 请求可能经历:内核通知连接可读;libuv 在合适阶段触发 Node 回调;解析器产生请求对象;用户 handler 同步执行;数据库客户端写 socket 后 await;主线程可处理其他就绪事件;上游响应到达后 Promise continuation 进入微任务;用户代码生成响应;底层根据 writable high-water mark 写出。任何长同步循环都会阻止同一 Isolate 中其他 JavaScript 前进,即使网络本身完全异步。
启动:入口之前已经发生了什么
node app.mjs 看似一条命令,实际输入还包括 Node 可执行文件、CLI flags、NODE_OPTIONS、当前目录、环境变量、package scope 和平台架构。排障必须保存完整命令而不是只保存入口文件。
node --version
node --help
node --permission --allow-fs-read=./data ./server.mjs第三条命令只用于支持相应 flag 的 Node 版本。入口文件本身也需要读取许可;如果 allow 范围没有覆盖入口或真实路径,启动会在业务代码之前以 ERR_ACCESS_DENIED 失败。软链接、相对路径和容器工作目录会改变“真实资源”判断,所以权限问题先记录 cwd、解析后的绝对路径和精确版本。
生产启动还要区分四类工作:同步配置解析、模块加载与初始化、外部依赖探测、开始监听。健康检查只能在服务真正可接受请求且关键初始化完成后成功。把大型 JSON、证书解析或依赖图初始化放在模块顶层,会增加冷启动并让导入测试产生副作用。
// server.mjs,Node 24 LTS;运行:node server.mjs
import { createServer } from 'node:http';
async function main() {
const config = await loadConfig();
const server = createServer((req, res) => route(req, res, config));
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(config.port, resolve);
});
return server;
}
const server = await main();把启动放进 main 使失败边界清晰,但不要为此引入启动框架。预期行为是:配置失败时进程以非零状态退出且不监听;端口占用时捕获 EADDRINUSE;监听成功后才报告 ready。
模块识别:扩展名、type 与调用方式
Node 同时支持 CommonJS(CJS)与 ECMAScript Modules(ESM)。.mjs 明确为 ESM,.cjs 明确为 CJS;.js 主要由最近 package scope 的 package.json#type 决定。缺少显式 marker 的歧义文件可能触发语法检测,包作者应显式声明 type,避免工具与运行时猜测。
| 输入 | 加载器/格式 | 关键边界 |
|---|---|---|
import './x.js' | ESM loader;格式由 URL、扩展名、package type 判断 | 相对 specifier 通常必须写扩展名;按 URL 缓存 |
import('pkg') | 动态 ESM loader | 返回 Promise;CJS 中也能调用 |
require('./x.cjs') | CJS loader | 同步求值并按 resolved filename 缓存 |
require('./sync-esm.mjs') | 支持版本可同步加载不含 top-level await 的 ESM | 遇 top-level await 不能同步完成 |
import 'node:fs' | 内建模块 | node: 清楚表达宿主依赖并避免名称歧义 |
Node v24 ESM 文档说明:ESM 导入 CJS 时,module.exports 作为可靠的 default;named exports 是静态分析得到的便利结果,不应依赖它捕获动态修改。反向互操作中,require() 只能同步取得满足条件且没有 top-level await 的 ESM。迁移策略应先把包边界显式化,再逐入口迁移,不要靠 transpiler 合成的 __esModule 猜测真实运行时行为。
exports、imports 和条件分支
包的 exports 限制公共子路径,并可按 import、require、node、default 等条件选择文件。条件顺序和调用方会改变结果,所以“同一个包名”可能在测试器、bundler、Node ESM 和 Node CJS 中落到不同文件。
{
"name": "catalog-core",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./schema": "./dist/schema.json"
}
}只导出实际承诺的公共入口。深层 import 被 exports 阻止是封装生效,不宜通过复制 node_modules 文件路径修补。排查 ERR_PACKAGE_PATH_NOT_EXPORTED 时记录请求 specifier、importer、加载方式和解析条件,再核对包版本的 exports;不要先加 alias 隐藏问题。
模块缓存与初始化副作用
CJS 以解析后的文件名缓存 module.exports;ESM 以 URL 标识模块记录。query/fragment 可能让同一文件以不同 URL 多次实例化,路径大小写在大小写不敏感文件系统上也可能产生意外身份。缓存保证的只是同一标识在当前 loader/realm 中复用,不是进程间、worker 间或部署实例间单例。
循环依赖会暴露“已建立但未完成初始化”的状态。CJS 常得到部分 exports;ESM live binding 受初始化时序约束,过早访问可能进入 temporal dead zone。最小修正通常是移动共享常量、让一方通过参数接收依赖,或把副作用从模块顶层移到显式函数,而不是把所有模块合并为一个文件。
事件循环:阶段、微任务与公平性
Node 的事件循环不是一个“任务队列”。libuv 的循环包含 timers、pending callbacks、poll、check、close callbacks 等阶段;Node 还在回调边界处理 process.nextTick 队列和 V8 microtask。阶段的细节会随版本调整,业务代码不应依赖两个到期 timer 的偶然顺序。
// order.mjs;运行:node order.mjs
import { readFile } from 'node:fs';
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));
readFile(new URL(import.meta.url), () => {
console.log('io');
process.nextTick(() => console.log('nextTick in io'));
queueMicrotask(() => console.log('microtask in io'));
});
console.log('sync');稳定结论只有 sync 先出现、I/O 回调内部的 nextTick/microtask 在后续 JavaScript 边界被清空;顶层 setTimeout(0) 与 setImmediate 的先后不应写成业务契约。process.nextTick 是 Node 专属且优先级很高,递归安排会饿死 I/O;普通 Promise 延续优先使用语言 microtask,但同样不能无界递归。
观察 event-loop lag
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
const delay = monitorEventLoopDelay({ resolution: 20 });
delay.enable();
setInterval(() => {
const elu = performance.eventLoopUtilization();
console.log({
p99Ms: Number(delay.percentile(99) / 1e6).toFixed(1),
utilization: elu.utilization.toFixed(3),
});
delay.reset();
}, 10_000).unref();event-loop delay 高表示计时器/回调未按期获得执行机会,但不自动说明根因。ELU 高且 CPU 高时查同步计算、序列化、正则或 GC;ELU 不高但请求慢时查上游和连接池;ELU 高、CPU 不满还可能是大量短回调、日志或容器调度。采样必须与 request trace 和 CPU profile 对齐。
libuv 线程池:不是无限并行 I/O
共享线程池服务部分 fs、DNS getaddrinfo、crypto 和 zlib 工作。大量昂贵 pbkdf2 可能拖慢同进程文件读取,看起来像磁盘退化。UV_THREADPOOL_SIZE 是进程启动时的容量选择,不是每个模块独享,也不是越大越快;线程过多增加内存和调度开销。
import { pbkdf2 } from 'node:crypto';
import { promisify } from 'node:util';
const derive = promisify(pbkdf2);
await Promise.all(Array.from({ length: 8 }, (_, index) =>
derive(`secret-${index}`, 'salt', 200_000, 32, 'sha256')
));这段代码只用于在受控环境观察线程池排队,不应用弱口令或固定 salt。运行时记录并发数、算法、iterations、CPU、线程池大小和文件 I/O 延迟。若任务是持续 CPU 工作,worker pool 往往比扩大 libuv pool 更清楚;若是密码哈希,优先使用经审查的库和独立资源预算。
Streams、backpressure 与内存上限
Readable 触发 data 不代表下游无限快。writable.write(chunk) 返回 false 表示内部缓冲达到 high-water mark,生产者应等待 drain;pipeline 会连接 backpressure、错误和关闭,比手写多层 .pipe()/事件监听可靠。
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
await pipeline(
createReadStream('./input.ndjson'),
createWriteStream('./copy.ndjson'),
);运行环境为 Node 22+,命令 node copy.mjs;成功时退出码为 0,目标文件内容相同。源文件不存在会以 ENOENT 失败。生产代码应在顶层捕获并设置非零退出码,且先写临时文件再原子替换,避免失败留下看似完整的目标。
Web Streams 与 Node streams 已有互操作入口,但对象模式、错误传播、取消和 backpressure 语义不能只靠类型相似判断。迁移到 Deno/Bun/Edge 时,边界尽量使用 Uint8Array、Request、Response 和 Web Streams;Node 专属库边界再转换,并以慢消费者测试内存曲线。
HTTP:超时、取消和连接不是一个数字
服务器需要区分 headers timeout、request timeout、keep-alive timeout、socket timeout 和业务 deadline。把所有问题交给一个 30 秒 timer 会留下慢速请求、半开连接或已经断开的上游工作。
import { createServer } from 'node:http';
const server = createServer(async (req, res) => {
const controller = new AbortController();
req.once('aborted', () => controller.abort(new Error('client aborted')));
try {
const upstream = await fetch('https://example.com/data', {
signal: AbortSignal.any([
controller.signal,
AbortSignal.timeout(2_000),
]),
});
res.writeHead(upstream.status, { 'content-type': 'application/json' });
res.end(await upstream.text());
} catch (error) {
if (!res.headersSent) res.writeHead(504);
res.end(JSON.stringify({ code: 'UPSTREAM_TIMEOUT' }));
}
});
server.requestTimeout = 10_000;
server.headersTimeout = 5_000;
server.listen(3000);示例为局部语义:上游 URL 与错误映射需按业务调整。生产代理大响应不应 text() 全量缓冲,而应流式转发;客户端断开后应取消仍可取消的上游请求。还要确认反向代理、负载均衡器和 Node 的超时顺序,外层应略长于内层或有明确所有者,避免随机一层先断开而无诊断信息。
CPU 并行:worker thread、进程与 cluster
JavaScript 主线程适合协调异步 I/O,不适合持续 CPU 密集计算。worker_threads 在同一进程创建独立 V8 Isolate,可通过 structured clone、transferable ArrayBuffer 或 SharedArrayBuffer 通信;共享进程意味着原生崩溃和总体内存仍可能影响全部 worker。child_process 有独立进程和更强故障隔离,但启动与通信更重。cluster 主要为多进程共享服务端口的历史方案;在容器编排中,多个单进程副本通常更直接。
不要每个请求新建 worker;启动和 Isolate 内存会吞掉收益。建立与 CPU/cgroup 配额匹配的有界 pool,队列满时明确拒绝或降级。消息体很大时测 structured clone 成本,能转移所有权的 ArrayBuffer 使用 transfer list。共享内存需要 Atomics 和协议验证,只有 profiling 证明复制是瓶颈时再引入。
内存、GC 与泄漏定位
V8 heap 只是 RSS 的一部分。Buffer、native addon、线程栈、代码页和共享库也占内存;因此 heapUsed 稳定而 RSS 上升不等于没有问题。先看趋势与负载关联,再选择 heap snapshot、sampling heap profiler、allocation timeline 或诊断报告。
| 现象 | 首选证据 | 常见根因 | 修正方向 |
|---|---|---|---|
| heapUsed 每轮回收后的低点持续上升 | 多个同阶段 heap snapshot、对象 retaining path | 无界 Map、listener、闭包、请求缓存 | 有界缓存、解除监听、缩短对象生命周期 |
| RSS 上升但 heap 稳定 | process.memoryUsage()、native/Buffer 指标 | 大 Buffer、碎片、addon、线程 | 流式处理、池上限、检查原生分配 |
| GC pause 与 CPU 上升 | trace/profile、分配热点 | 短命大对象、JSON 全量转换 | 减少复制、流式解析、控制并发 |
| OOM 前无日志 | diagnostic report、容器事件、heap near limit | cgroup 限制低于预期、突发并发 | 设置资源预算、拒绝过载、保留报告 |
heap snapshot 可能暂停进程、占用额外内存并含敏感数据,生产采集要受控。不要在已经接近内存上限的唯一副本上随意生成完整 snapshot;先复现到 staging 或使用采样工具。
权限模型与真实安全边界
Node Permission Model在 v22.13/v23.5 起标记稳定,并通过 --permission 限制文件、网络、子进程、worker、addon、WASI、FFI 与 inspector 等能力。它适合把“该进程不需要的宿主能力”变成可验证策略,但官方文档也列出约束:模型在环境初始化后建立,部分预初始化 flag 不受同样规则;worker 权限继承与原生能力需要逐版本核对。
权限模型不是恶意多租户代码沙箱。进程内依赖仍能读写所有已允许范围,CPU/内存拒绝服务、prototype pollution、应用授权错误和 SSRF 都需另行控制。运行不可信代码使用 OS/container/VM 级隔离、资源配额、网络策略与一次性凭据,并把 Node 权限作为附加层。
高频信任边界
- 路径:用固定根目录和
resolve后的包含关系验证,防止../、绝对路径、软链接越界;不要仅替换字符串。 - 网络:解析和固定允许的 scheme/host/port,防 SSRF 到 metadata、loopback 与私网;重定向后重新检查。
- 子进程:优先
execFile/spawn参数数组,不把不可信输入拼进 shell;限制可执行文件和环境。 - secret:按名称读取最小环境变量,不打印 env;错误响应不含堆栈、绝对路径和连接串。
- addon:
.node原生扩展与安装脚本拥有进程级能力,需锁版本、校验来源和平台 ABI,不能依赖 JS 权限幻想。
优雅关闭与进程正确性
收到 SIGTERM 后立即 process.exit() 会截断响应和日志;只调用 server.close() 又可能被长连接无限拖延。正确关闭需要状态机与上限:标记 not ready,停止接受新请求,关闭 keep-alive/空闲连接,等待有界在途工作,刷新必要遥测,超时后强制退出。
let shuttingDown = false;
const inflight = new Set();
server.on('request', (req, res) => {
const token = {};
inflight.add(token);
res.once('close', () => inflight.delete(token));
});
process.once('SIGTERM', async () => {
if (shuttingDown) return;
shuttingDown = true;
server.close();
const deadline = AbortSignal.timeout(8_000);
while (inflight.size && !deadline.aborted) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
process.exitCode = inflight.size ? 1 : 0;
});这是状态机骨架,不包含框架特定连接管理。Node 当前版本还提供关闭空闲/全部连接的服务器 API,使用前按支持版本核对。Kubernetes 等平台中,termination grace period 必须大于应用 deadline,并留出代理摘流量时间。写操作必须有幂等键,因为客户端可能在响应丢失后重试。
未捕获异常和未处理 rejection 代表程序状态可能未知。顶层记录最小安全上下文并让进程退出,由 supervisor 拉起;不要捕获后继续永久服务。退出码、信号和 core/diagnostic report 是事故证据的一部分。
可观测性:先关联,再采样
每个请求携带或生成 request id,使用 AsyncLocalStorage 可以在异步链上传递上下文,但它不是业务状态仓库。高基数用户输入不要直接成为指标 label。日志回答单次发生了什么,metrics 显示趋势与告警,trace 串联跨服务时序,profile 解释 CPU/分配热点,diagnostic report 保存崩溃附近进程事实。
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
const context = new AsyncLocalStorage();
function withRequest(req, fn) {
const requestId = req.headers['x-request-id'] || randomUUID();
return context.run({ requestId }, fn);
}
function log(message, fields = {}) {
console.log(JSON.stringify({
time: new Date().toISOString(),
requestId: context.getStore()?.requestId,
message,
...fields,
}));
}第三方 thenable、手写 callback 桥接或 worker 边界可能丢上下文;以集成测试验证关键链,并显式把 trace context 传入 worker/message。日志需限制 body/header、脱敏凭据和换行注入,错误对象序列化要保留 name/code/cause 而不是把所有内容发给客户端。
性能实验:从正确性到尾延迟
运行时比较至少分离:进程启动、模块冷加载、稳态 HTTP、CPU 任务、文件/数据库 I/O、流与内存。先验证响应、错误和取消语义相同,再固定机器、架构、Node flags、依赖、连接复用、并发、响应体和观测开销。记录 warm-up 和原始 samples,而不是只保存均值。
Node 的 JIT 会随热点改变代码层级,GC 会制造延迟分布,容器 CPU throttling 会让 wall time 上升。p99 必须同时看 event-loop delay、GC、上游和系统 CPU。吞吐提升但错误率/排队/内存失控不是优化。对短 CLI,启动与依赖加载可能占主导;对长服务,稳态、尾延迟和恢复更重要。
故障排查顺序
| 现象 | 根因候选 | 最小证据 | 不应先做 |
|---|---|---|---|
| 本地能 import,CI 不能 | 大小写、cwd、Node 版本、条件导出 | specifier/importer/version/文件树 | 删除 lockfile 并全量升级 |
| 延迟周期性尖峰 | 同步 CPU、GC、日志、定时批任务 | p99 时间对齐 CPU/GC/event-loop | 只增加实例掩盖单实例阻塞 |
| 文件读取随密码任务变慢 | libuv pool 竞争 | 固定并发对照与线程池队列 | 假设“异步就不争资源” |
| 进程不退出 | ref 句柄、server/socket、timer | getActiveResourcesInfo、诊断报告 | 末尾无条件 process.exit(0) |
| 内存上升 | JS retained、Buffer/native、无界并发 | heap 与 RSS 分解、snapshot/profile | 只调大 old-space |
| SIGTERM 时 502 | 摘流量、server close、grace period 顺序错 | shutdown 时间线和在途计数 | 无限等待或立即退出 |
版本与升级策略
生产只部署受支持 Node 版本。LTS 不表示所有 API 永远不变,也不替代依赖测试;它表示维护窗口和修复策略更适合生产。跨 major 升级安排组合测试:纯 ESM/CJS fixture、条件导出、native addon、test runner、权限 flags、HTTP 超时、TLS/crypto、诊断代理和容器镜像。先在 CI 双版本运行,再 staging 回放代表流量,最后小比例发布。
Node 26 Current 可以用于验证未来兼容,但不应仅因版本号更新就替换 24 LTS。反过来,继续使用 EOL 版本意味着安全和关键缺陷不再按官方计划修复,应作为明确风险而非“稳定所以不动”。
官方资料与继续阅读
- Node.js Releases:版本状态与发布策略。
- Node.js v24 ESM 与 Packages:模块识别、解析、互操作与条件导出。
- The Node.js Event Loop:事件循环与
nextTick说明。 - Node.js Permission Model:权限范围、flags 与已知约束。
- Worker Threads、Diagnostic Report、Performance Hooks:并行与诊断入口。
- Node.js GitHub:Release、Issue、PR 和源码证据;解释异常版本行为时应链接到具体记录。
交付检查
完成 Node.js 学习练习时,应留下:精确 LTS 版本和镜像;显式 package type 与公共 exports;模块解析 fixture;HTTP 超时/取消/流契约;event-loop 与线程池故障实验;CPU worker 的有界队列;权限 allow/deny 记录;SIGTERM 时间线;p95/p99、错误率、CPU、RSS 与原始样本。具备这些证据,才算从“会写 Node 服务”进入“能解释并运维 Node 运行时”。
运维演练:从健康检查到事故复盘
一套最小演练比堆叠监控依赖更能暴露运行时假设。先准备四个只在测试环境启用的故障入口:同步占用主线程一小段时间、提交一组线程池任务、流式输出固定大小数据、登记一个可取消的慢上游。每个入口必须有并发和时长上限,不能把故障开关暴露到公网。
第一轮记录初始数据。空闲进程记录启动时间、RSS、heap、活跃资源;固定并发请求记录成功率、p50/p95/p99、event-loop delay 和 CPU。第二轮只开启同步阻塞,预期所有同 Isolate 请求的延迟一起上升,CPU profile 指向故障函数。第三轮只增加 crypto 与文件操作,若文件延迟随线程池队列上升,应能通过分组实验说明竞争,而非把问题归咎于磁盘。第四轮让客户端缓慢读取大响应,正确实现的 RSS 应保持在有界范围;若内存随响应总量/并发线性上升,说明中间层发生 buffering。
随后发送 SIGTERM。readiness 应先失败,新请求不再进入;已接收请求在 deadline 内完成;超时请求得到取消或连接关闭;进程退出码与是否强制终止一致。重复信号不能执行两次资源清理。若应用在反向代理之后,还要确认代理停止转发的时间早于进程退出,且 keep-alive 连接不会绕过摘流量。
最后产出复盘,不只写“优化了代码”。复盘列出请求时间线、首个异常指标、CPU/heap/diagnostic 证据、错误阶段的拥有者、修复的机制、回归实验和回滚方式。若修复只是提高 timeout/heap/线程池大小,必须说明原负载为什么合理以及新上限的容量依据;否则它只是把失败推迟。
容量估算的最低要求
Node 服务容量不能只写每秒请求数。至少同时记录每实例可持续并发、单请求 CPU、event-loop p99、上游连接数、线程池工作、峰值 RSS 和 shutdown drain 时间。用 Little's Law 的直觉检查:到达率乘平均在系统时间会形成在途数量;上游变慢即使吞吐未变,也会增加内存与连接。队列必须有界,满时用 429/503、Retry-After 或业务降级明确反馈,不能继续接受直至 OOM。
扩容前先确认瓶颈。CPU 饱和且请求可横向分割时加实例;单实例主线程阻塞先修同步热点;数据库连接耗尽时加应用实例可能恶化数据库;线程池竞争应拆工作或限并发;内存泄漏不能靠频繁重启作为永久方案。扩容结论需把容器 CPU quota 纳入,因为 Node 看到的主机 CPU 数量不一定等于实际可用配额,worker 数不能机械取 os.cpus().length。
依赖升级演练
升级 Node major 或关键框架前,固定旧/新镜像做双轨测试。模块 fixture 检查 exports 选择、CJS/ESM namespace、top-level await 和 loader;HTTP fixture 检查 header/timeout/stream;native fixture 在每个目标架构加载;诊断 fixture 验证 source map、async context、profile 与 agent。记录 deprecation warning,区分来自应用、依赖还是 flag。只有错误契约和 shutdown 都通过后才做性能对照,避免更快地返回错误也被计为收益。