每次调用 model.save(),Mongoose 内部都会走一遍 path 校验、默认值填充、脏状态标记,再把这些状态包装成 Promise 返回。当批量写入或高频更新时,这些中间 Promise 分配和状态计算的开销会悄悄累积。9.7.1 的核心改动就是砍掉不必要的 Promise 分配、压缩 path/default/dirty-state 的处理路径,让 save() 跑得更轻。同时 schema.toJSONSchema() 修了一个容易踩到的坑——ObjectId 字段现在会带上 regex pattern。
save 性能优化到底改了什么
PR #16331 做了三件事:
- 减少 Promise 分配——内部流程中部分步骤原本会创建 Promise 再立即 resolve,现在直接同步返回,跳过微任务队列的排队。
- 压缩 path 处理开销——遍历 schema path 时减少重复计算,尤其是嵌套文档和数组场景。
- 优化默认值与脏状态追踪——
defaultValue赋值和dirtyPath标记的内部数据结构做了精简,避免每次 save 都重建。
对开发者来说,API 没有任何变化,save() 的返回值仍然是 Promise,只是内部少走了几步弯路。在大量小文档高频写入的场景下(比如日志采集、事件流水),提升会比较明显。
toJSONSchema 的 ObjectId regex 修复
用 schema.toJSONSchema() 生成 JSON Schema 时,ObjectId 类型的字段之前不会输出 pattern 属性。如果你拿这个 Schema 做 API 参数校验(比如 ajv),ObjectId 字段就等于没有格式约束,任何字符串都能通过。9.7.1 修复后,ObjectId 会带上符合 24 位十六进制字符串的 regex pattern。
实际对比:升级前后跑一遍
下面是一个可以直接运行的小测试脚本,用 bench 模块对比同一模型在大量 save 下的耗时。先安装依赖:
npm install mongoose bench
然后运行脚本(确保本地或远程有可连接的 MongoDB 实例,修改 MONGO_URI 为你的连接地址):
const mongoose = require('mongoose');
const bench = require('bench');
const MONGO_URI = 'mongodb://localhost:27017/mongoose_bench';
const LogSchema = new mongoose.Schema({
level: { type: String, default: 'info' },
message: { type: String, required: true },
source: { type: String, default: 'app' },
traceId: { type: mongoose.Schema.Types.ObjectId },
createdAt: { type: Date, default: Date.now }
});
const Log = mongoose.model('Log', LogSchema);
async function runSaveBench(count) {
await mongoose.connect(MONGO_URI);
await Log.deleteMany({});
const start = Date.now();
for (let i = 0; i < count; i++) {
const doc = new Log({ message: `msg-${i}`, traceId: new mongoose.Types.ObjectId() });
await doc.save();
}
const elapsed = Date.now() - start;
console.log(`${count} saves took ${elapsed}ms (${(elapsed / count).toFixed(2)}ms per save)`);
// 看看 toJSONSchema 对 ObjectId 的输出
const jsonSchema = LogSchema.toJSONSchema();
console.log('traceId in JSON Schema:', JSON.stringify(jsonSchema.properties.traceId, null, 2));
await mongoose.disconnect();
}
runSaveBench(500).catch(err => { console.error(err); process.exit(1); });
升级到 9.7.1 后再跑一次,对比单次 save 平均耗时的变化。traceId 字段在 toJSONSchema() 输出中应该能看到 pattern 属性:
{
"type": "string",
"pattern": "^[0-9a-fA-F]{24}$"
}
如果你的项目用 ajv 或类似工具做请求校验,这个 pattern 直接就能生效,不用再手动补规则。
升级建议与注意事项
- 无 API 变更,直接
npm install mongoose@9.7.1即可,不需要改业务代码。 - 性能提升在高频小文档写入场景最明显;低频大文档场景变化不大,但也没有副作用。
- 如果你依赖
toJSONSchema()生成 API 校验规则,升级后 ObjectId 字段会自动带 pattern,之前靠手动补正则的代码可以清理掉。 - 大规模项目建议先在 staging 环境跑一遍写入压测,确认耗时符合预期后再上线。
9.7.1 是一个典型的「内部瘦身」版本——接口不动,底层少做无用功。对于已经在用 Mongoose 的项目,升级成本几乎为零,收益却是实打实的写入提速和 Schema 校验补全。