惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
博客园 - 三生石上(FineUI控件)
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园_首页
Microsoft Azure Blog
Microsoft Azure Blog
GbyAI
GbyAI
腾讯CDC
酷 壳 – CoolShell
酷 壳 – CoolShell
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Jina AI
Jina AI
爱范儿
爱范儿
博客园 - 【当耐特】
雷峰网
雷峰网
S
SegmentFault 最新的问题
美团技术团队
Blog — PlanetScale
Blog — PlanetScale
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
J
Java Code Geeks

Echo JS

GitHub - evoluteur/cymatics: Play a frequency and watch the sand settle into its Chladni figure, computed from the wave equation. Memdeklaro - The Basics of Decentralized Identity (DID) and Self-Sovereign Identity (SSI) How Railmid Works GitHub - evoluteur/platonic-solids: Turn the five Platonic solids in 3D, show their duals, read their measurements, and print the nets to fold your own. Sharing Application State in a URL GitHub - evoluteur/sacred-geometry: Sacred Geometry Generator: draw, tune, and export Vesica Piscis, Seed of Life, Flower of Life, Metatron's Cube, and the Golden Spiral as SVG Best of Self-Sovereign Identity: Digitalcourage, World Passport and Memdeklaro Reads Are Subscriptions - Migrating from Zustand to Coaction GitHub - evoluteur/binaural-beats: Simple web page to play binaural beats for sleep, meditation, relaxation, and focus: Delta, Theta, Alpha, Beta, and Gamma brainwave frequencies, with an optional pink or brown noise bed. toast-queue — Accessible, customizable toast notifications Building a High-Performance Data Grid in React, Vue, and Svelte I built a flight recorder for AI sessions React Authentication With JWT, Zustand, and Axios | JavaScript Tools Blog My idempotency library had one job. A dropped connection made it run the payment twice. "half-open" twice is not the same state: the bug that shaped breakwater 1.0 GitHub - evoluteur/evolutility-server-node: Framework for building REST APIs for CRUD with models rather than code (using Node.js, Express, and PostgreSQL). React Router v8 in Action: Lazy Loading and Nested Routes One $ for every environment | Xec My test suite had 100% coverage. Mutation testing still found real bugs. The type-safe data layer for Kysely | Kysera JavaScript Obfuscation in the AI Era | JavaScript Tools Blog Using Mongoose Studio with Apache Cassandra via Data API GitHub - trekhleb/yesbrainer: 🧠 A council of AI models for the decisions that aren't no-brainers — they answer in parallel, debate to consensus, or get judged to a verdict. Browser-only, open source, bring your own keys (BYOK), no backend. Node.js has plenty of circuit breakers. So why did I build another one? My Redis library said the write succeeded. Redis was down. GitHub - Techthos/gadget: Prebuilt, interactive HTML widgets for MCP Apps in Go — data tables and forms, self-contained in a single binary, host-themed, spec-compliant. GitHub - evoluteur/react-morph-charts: React component for bubble chart, bar chart, and pie chart, with animated morphing transitions between charts, on hover, and on window resize. Interactive Metaballs Tutorial GitHub - mutativejs/travels: A fast, framework-agnostic undo/redo core powered by Mutative JSON Patch Nim: A Python-Like C/C++ Alternative Every Programmer Should Learn
What
by Valeri Karpov · 2026-07-31 · via Echo JS

Mongoose 9.9.0 was released on July 30, 2026 and is primarily focused on improving performance. We've been looking forward to taking a deep dive into JavaScript performance for many years and we're thrilled to share our first round of improvements. These changes reduce overhead in several of Mongoose's most heavily used code paths and will make most Mongoose apps a little bit faster with no changes required. This release includes several optimizations for insertMany(), document validation, and toObject().

Most importantly, these changes do not bypass casting, change tracking, validation, middleware, getters, transforms, or any other Mongoose feature that your app relies on. Mongoose is just now smarter about the bookkeeping it needs to do to support these features.

In this blog post, I'll cover the most important performance improvements in Mongoose 9.9, with a particular focus on insertMany().

Faster insertMany()

insertMany() is a convenient way to insert a large number of documents while still getting Mongoose casting, defaults, validation, timestamps, and middleware. Mongoose does additional work to validate and hydrate each document, but there was also a lot of overhead that we could eliminate. All in all, we've been able to improve Mongoose's performance on our insertManySimple benchmark by 35%.

Mongoose insertMany() 1500 documentsA bar chartMongoose insertMany() 1500 documentsMongoose 9.9.0 vs 9.8.1 vs MongoDB Node.js driver13.58msmongoose@9.8.18.75msmongoose@9.9.05.31msmongodb@7.5.0ms

Here's some of the biggest pieces of overhead that we were able to eliminate:

Using empty objects {} instead of new Map()

Creating a new Map is 10x slower than creating an empty object - creating an empty object is heavily optimized in V8, even faster than using Object.create(null).

We also entirely got rid of originalDocIndex, which was one of the maps insertMany() used to track order. I originally started moving toward using new Map() to minimize risk of prototype pollution, but the performance tradeoff is too great to justify in performance-sensitive code.

Leveraging Mongoose's fast path toObjectShallow() function to avoid deep cloning for documents that only have primitives.

Mongoose's toObject() function deep clones the document. toObjectShallow() is a faster alternative that skips all recursive cloning - about 40% faster than toObject() because it avoids a bunch of unnecessary options, object checks, and recursive cloning.

We also reuse the check for whether the document has only primitive values to avoid checking for subdocuments when resetting isNew.

Faster setting of versionKey.

Before, insertMany() would initialize the version key using doc[versionKey] = 0, which looks fine at first glance... until you realize that doc[versionKey] actually triggers Mongoose's entire set() path, including setters and checking versionKey for . to potentially split the version key path.

Now, insertMany() uses Mongoose's internal $__setValue() function with a pre-split version key path to avoid the extra indexOf() check and general set() overhead. In particular, Mongoose documents' $__setValue() now has a fast path for when the given path is an array of length 1 - in this case, it simply sets the value directly without any indexOf() or split() overhead.

Faster parallelLimit() without sets

insertMany() uses a parallelLimit() function to ensure a maximum of options.limit validations can execute concurrently - this is helpful for async validators that make database queries. Previously, parallelLimit() used a Set to track pending validations: parallelLimit starts a validation, adds it to the set, waits for the next validation to complete using Promise.race(), and removes the completed validation from the set. Below is the old code.

const results = [];
const executing = new Set();

for (let index = 0; index < params.length; index++) {
  const param = params[index];
  const p = fn(param, index);
  results.push(p);

  executing.add(p);

  const clean = () => executing.delete(p);
  p.then(clean).catch(clean);

  if (executing.size >= limit) {
    await Promise.race(executing);
  }
}

return Promise.all(results);

Like with maps, creating a new Set() is prohibitively slow. Also, calling Promise.race() O(n) times is slow because Promise.race() creates a new promise every time and iterates over all pending promises. Instead of relying on a set, the new optimized insertMany() kicks off limit "worker" functions. Each worker() call executes a single validation, and recursively calls worker() when it succeeds. That means no set instantiation and no Promise.race(): parallelLimit() tracks the next validation to execute with a single nextIndex counter.

Change tracking improvements

Mongoose's state machine class is simple, but it sits on a very hot path: documents use it on every property set and after a document is persisted in insertMany() or save(). The StateMachine class stores:

  1. A states object that maps state names to a list of paths in that state
  2. A paths object that maps paths to their state - the inverse of states

The state machine class has a clear() method clears all paths from a given state.

StateMachine.prototype.clear = function clear(state) {
  if (this.states[state] == null) {
    return;
  }
  const keys = Object.keys(this.states[state]);
  if (keys.length === 0) {
    return;
  }
  this.states[state] = {};
  let i = keys.length;

  while (i--) {
    delete this.paths[keys[i]];
  }
};

While this function may look inocuous at first glance, it is actually quite expensive. Specifically, the delete in a loop is a massive JIT deopt. While the overhead for a single clear() call is negligible, Mongoose calls clear() twice for every single document in the insertMany(). When calling insertMany() on 1500 documents 10,000 times this small overhead adds up to a significant performance hit. However, we don't need to call clear() - the idea is that Mongoose calls clear() multiple times to clear almost all the change tracking for each document, minus the init state. So instead of calling clear() multiple times, we can define a separate clearAllExcept() method that clears all states except for the given state. This avoids the delete deopt.

StateMachine.prototype.clearAllExcept = function clearAllExcept(state) {
  // State buckets are created lazily, so `states[state]` may not exist yet.
  const bucket = this.states[state];
  const keys = bucket == null ? [] : Object.keys(bucket);
  if (keys.length === 0) {
    this.paths = {};
    this.states = {};
    return;
  }
  this.paths = {};
  for (const path of keys) {
    this.paths[path] = state;
  }
  this.states = {
    [state]: this.states[state]
  };
};

The change tracking improvements don't just apply to insertMany(); save() also calls clear() under the hood. Specifically, avoiding clear() combined with some optimizations to getting paths to validate() means creating and saving a new document with 10 properties is now about 10% faster, albeit still 15% slower than the MongoDB Node.js driver.

Mongoose save() vs insertOne()A bar chartMongoose save() vs insertOne()Mongoose 9.9.0 vs 9.8.1 vs raw MongoDB Node driver for creating a new document with 10 properties2.61msmongoose@9.8.12.35msmongoose@9.9.02msmongodb@7.5.0ms

More efficient toObject()

Mongoose's toObject() and toJSON() methods are heavily used even when they aren't called directly by the application. For example, when you call JSON.stringify() on a Mongoose document, JSON.stringify() calls toJSON() internally. And Express.js' res.json() calls JSON.stringify() internally. Also, the save() function uses toObject() internally when creating a new document.

First, Mongoose now caches which schema paths have transforms. Before, even if your schema had no transforms, toObject() would still scan every path in your schema to check for transforms. In the common case where a schema has no transforms, toObject() sees that the list of transforms is null and skips the entire transform check. Checking for null is also slightly faster than checking if an array has length === 0.

Similarly, applying getters now skips paths that do not have getters before checking whether the path is selected by caching which paths have getters. Mongoose no longer needs to scan every path in your schema to check for getters. This improvement is even more significant when working with complex projections. In Mongoose 9.8, Mongoose would scan every path in your schema and check whether the path was included in the projection, which could be O(n*p) where n is the number of paths in your schema and p is the number of paths in the projection. Mongoose 9.9 avoids checking whether the path is included in the projection for paths that do not have getters - if your schema still has a lot of getters, like if you use global getters to automatically convert ObjectIds to strings, you would see less of a performance improvement.

Timestamp updates only add createdAt for upserts

When applying timestamps to an update, Mongoose needs to set updatedAt on every update. However, $setOnInsert is guaranteed to be ignored if upsert is not set, so setting createdAt in $setOnInsert without upsert is a waste of memory and bandwidth. Mongoose 9.9 now avoids adding $setOnInsert.createdAt to ordinary updates:

const userSchema = new mongoose.Schema({ name: String }, { timestamps: true });
const User = mongoose.model('User', userSchema);

// No longer sets `createdAt` in `$setOnInsert`
await User.updateOne(
  { name: 'John' },
  { $set: { name: 'John Smith' } }
);

// Only add `createdAt` to `$setOnInsert` if `upsert: true` is set
await User.updateOne(
  { name: 'John' },
  { $set: { name: 'John Smith' } },
  { upsert: true }
);

This produces an update with updatedAt, but does not add a createdAt value that can never be used by the non-upsert operation. For an upsert, Mongoose still adds createdAt to $setOnInsert as expected.

Moving On

Mongoose 9.9 features widespread performance improvements that will help most Mongoose apps run a little bit faster. The changes we made are all fairly small, but they add up to a noticeable performance boost in insertMany() and document serialization, particularly in documents with no nested paths.

As always, if you are upgrading a production application, run your existing test suite and test it out for yourself. The benchmarks in this release are intentionally simple; schemas with nested documents, custom validators, middleware, getters, and transforms will have different performance characteristics.

Install Mongoose 9.9 and try it out for yourself!

npm install mongoose@9.9

Found a typo or error? Open up a pull request! This post is available as markdown on Github