性能优化指南

建议

  • 复用同一个 schema 对象,让默认验证器缓存命中。
  • 通过 s.config({ cache })new Validator({ cache }) 调整缓存策略。
  • 对热点路径避免在循环中重复构造 DSL。
  • 服务端应复用长生命周期的 Validator 实例;每次请求都新建会重置验证引擎和实例缓存。
  • 如果你已经自行管理底层验证实例,再考虑 SchemaUtils.validateBatch() 这类批量路径。

当前 benchmark 基线

本节全部数值来自仓库跟踪的 test/benchmarks/performance-docs-snapshot.json,不再拼接不同 benchmark 报告。环境:Node.js v20.20.2、win32-x64、Zod 4.3.6;运行开始时间 2026-07-13T09:55:17.579Z。

该 full 场景矩阵包含 19 个计入胜负的可比场景和 1 个不计入胜负的异步抛错诊断场景(AV2_THROW);可比场景中 schema-dsl 胜 14/19,Zod 胜 5/19。接近持平的场景可能在不同运行中切换胜方,因此这个矩阵只作为本仓库的回归信号,不作为永久公开性能承诺。

ID场景schema-dslZod结果
S1有效数据1.792M1.427Mschema-dsl 1.26x
S2无效数据158.10K12.77Kschema-dsl 12.38x
S3格式化14.19K13.48Kschema-dsl 1.05x
C1类型转换3.852M3.221Mschema-dsl 1.20x
C2关闭类型转换635.40K29.86Kschema-dsl 21.28x
U1联合类型2.723M10.295MZod 3.78x
U2联合类型2.690M5.977MZod 2.22x
E1枚举10.370M14.025MZod 1.35x
A1数组1.061M268.34Kschema-dsl 3.95x
A2数组33.43K27.63Kschema-dsl 1.21x
D1深层对象777.96K2.101MZod 2.70x
L1大对象110.41K83.25Kschema-dsl 1.33x
COND1条件分支10.30K17.48KZod 1.70x
COND2条件分支9.48K7.80Kschema-dsl 1.22x
CV1自定义规则6.747M6.090Mschema-dsl 1.11x
CV2自定义规则182.50K33.26Kschema-dsl 5.49x
AV1异步1.943M1.021Mschema-dsl 1.90x
AV2异步39.00K38.94Kschema-dsl 1.00x
AV2_THROW异步抛错40.55K29.75Kschema-dsl 1.36x
COLD1冷启动13.60K7.34Kschema-dsl 1.85x

矩阵的 JSON 报告会记录语义差异。这里比较的是双方最接近的受支持行为,不表示所有场景的内部实现语义完全相同。

这些数字适合作为当前项目的回归基线。Node.js、依赖、schema 复杂度或错误格式化行为变化后,应重新运行 benchmark。

推荐做法

import { Validator, s } from 'schema-dsl/pure';

const schema = s({
	email: 'email!',
	age: 'number:18-100'
});

const validator = new Validator({
	cache: { maxSize: 500, statsEnabled: true }
});

validator.validate(schema, { email: 'a@example.com', age: 20 });
validator.validate(schema, { email: 'b@example.com', age: 21 });

console.log(validator.getCacheStats());

请求级 DSL 与内存边界

调用 s() 本身不会保留无限增长的全局状态。生产环境真正需要避免的不是“每次请求调用 s() 必然泄漏”,而是在热点路径上生成无限多种不同的 schema 结构。

// 通常安全,但仍慢于启动时转换:
// 结构稳定,验证阶段可以复用编译缓存。
app.post('/users', (req, res) => {
	const schema = s({ email: 'email!', age: 'number:18-100' });
	const result = validate(schema, req.body);
	res.json(result);
});

// 长运行服务中应避免:
// 每个请求都生成不同结构,缓存几乎无法命中。
app.post('/dynamic', (req, res) => {
	const schema = s({ [`field_${req.id}`]: 'string!' });
	const result = validate(schema, req.body);
	res.json(result);
});

内置缓存对“相同 schema 结构被重复使用”的场景有效。它无法让无限多、从不重复的新 schema 变便宜:schema-dsl 自身的受控缓存有容量边界,但每次未命中仍要承担转换和 validator 编译成本。

服务端 Validator 生命周期

普通 API 请求中不要每次都创建新的 Validator

// 不推荐放在请求处理函数里
app.post('/users', (req, res) => {
	const validator = new Validator();
	res.json(validator.validate(userSchema, req.body));
});

如果实例没有被长期持有,这通常不是长期内存泄漏;问题在于它会丢弃实例级缓存,并让新的 validator / 缓存对象不断进入分配和 GC。推荐使用一个应用级 validator,或按不同配置 profile 维护少量 validator。

对确实不可复用、基数很高的一次性动态 schema,可以隔离这条路径并让 validator 短生命周期存在,但不要把请求内创建的 validator 或 schema 对象存入应用级集合。

什么时候需要更低层优化

  • 你要长期复用同一个 Validator 实例并观察命中率。
  • 你需要显式控制缓存大小、TTL 或统计开关。
  • 你已经维护底层验证实例,需要走 SchemaUtils.validateBatch()

验证命令

npm test
npm run bench:smoke
npm run bench:conditional
npm run bench:full
npm run bench:cache
npm run bench:guard:smoke
npm run bench:guard:full

回归门禁会对每个被跟踪的场景运行三次并取中位数。同一 Node.js、平台和 CPU 下,schema-dsl/Zod 比值必须不低于基线的 75%;绝对吞吐低于阈值时,只有同轮 Zod 也出现相同比例的主机负载下降才标记为 CALIBRATED,否则仍失败。环境不同时只使用同轮相对比值。这里的 Zod 是固定版本的校准负载,不构成产品性能宣传。


对应示例文件

示例入口: performance-guide.ts
说明: 展示复用同一个 schema / validator、读取缓存统计,以及 SchemaUtils.withPerformance() 包装后的耗时输出。