Mongoose 9.7.1:省掉多余的 Promise,save 写入更快了

2026-06-18 41 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:5 分钟

每次调用 model.save(),Mongoose 内部都会走一遍 path 校验、默认值填充、脏状态标记,再把这些状态包装成 Promise 返回。当批量写入或高频更新时,这些中间 Promise 分配和状态计算的开销会悄悄累积。9.7.1 的核心改动就是砍掉不必要的 Promise 分配、压缩 path/default/dirty-state 的处理路径,让 save() 跑得更轻。同时 schema.toJSONSchema() 修了一个容易踩到的坑——ObjectId 字段现在会带上 regex pattern。

save 性能优化到底改了什么

PR #16331 做了三件事:

  1. 减少 Promise 分配——内部流程中部分步骤原本会创建 Promise 再立即 resolve,现在直接同步返回,跳过微任务队列的排队。
  2. 压缩 path 处理开销——遍历 schema path 时减少重复计算,尤其是嵌套文档和数组场景。
  3. 优化默认值与脏状态追踪——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 校验补全。


相关推荐