Faster Mongoose Queries With Lean

The lean option tells Mongoose to skip hydrating the result documents. This makes queries faster and less memory intensive, but the result documents are plain old JavaScript objects (POJOs), not Mongoose documents. In this tutorial, you'll learn more about the tradeoffs of using lean() .

By default, Mongoose queries return an instance of the Mongoose Document class. Documents are much heavier than vanilla JavaScript objects, because they have a lot of internal state for change tracking. Enabling the lean option tells Mongoose to skip instantiating a full Mongoose document and just give you the POJO.

const leanDoc = await MyModel.findOne().lean();

How much smaller are lean documents? Here's a comparison.

const schema = new mongoose.Schema({ name: String }); const MyModel = mongoose.model( 'Test' , schema); await MyModel.create({ name: 'test' }); const sizeof = require ( 'object-sizeof' ); const normalDoc = await MyModel.findOne(); const leanDoc = await MyModel.findOne().lean(); sizeof(normalDoc); sizeof(leanDoc); JSON .stringify(normalDoc).length === JSON .stringify(leanDoc.length);

Under the hood, after executing a query, Mongoose converts the query results from POJOs to Mongoose documents. If you turn on the lean option, Mongoose skips this step.

const normalDoc = await MyModel.findOne(); const leanDoc = await MyModel.findOne().lean(); normalDoc instanceof mongoose.Document; normalDoc.constructor.name; leanDoc instanceof mongoose.Document; leanDoc.constructor.name;

The downside of enabling lean is that lean docs don't have:

Change tracking

Casting and validation

Getters and setters

Virtuals

save()

For example, the following code sample shows that the Person model's getters and virtuals don't run if you enable lean .

const personSchema = new mongoose.Schema({ firstName: { type: String , get: capitalizeFirstLetter }, lastName: { type: String , get: capitalizeFirstLetter } }); personSchema.virtual( 'fullName' ).get( function ( ) { return ` ${this.firstName} ${this.lastName} ` ; }); function capitalizeFirstLetter ( v ) { return v.charAt( 0 ).toUpperCase() + v.substr( 1 ); } const Person = mongoose.model( 'Person' , personSchema); await Person.create({ firstName: 'benjamin' , lastName: 'sisko' }); const normalDoc = await Person.findOne(); const leanDoc = await Person.findOne().lean(); normalDoc.fullName; normalDoc.firstName; normalDoc.lastName; leanDoc.fullName; leanDoc.firstName; leanDoc.lastName;

Populate works with lean() . If you use both populate() and lean() , the lean option propagates to the populated documents as well. In the below example, both the top-level 'Group' documents and the populated 'Person' documents will be lean.

const Group = mongoose.model( 'Group' , new mongoose.Schema({ name: String , members: [{ type: mongoose.ObjectId, ref: 'Person' }] })); const Person = mongoose.model( 'Person' , new mongoose.Schema({ name: String })); const people = await Person.create([ { name: 'Benjamin Sisko' }, { name: 'Kira Nerys' } ]); await Group.create({ name: 'Star Trek: Deep Space Nine Characters' , members: people.map(p => p._id) }); const group = await Group.findOne().lean().populate( 'members' ); group.members[ 0 ].name; group.members[ 1 ].name; group instanceof mongoose.Document; group.members[ 0 ] instanceof mongoose.Document; group.members[ 1 ] instanceof mongoose.Document;

Virtual populate also works with lean.

const groupSchema = new mongoose.Schema({ name: String }); groupSchema.virtual( 'members' , { ref: 'Person' , localField: '_id' , foreignField: 'groupId' }); const Group = mongoose.model( 'Group' , groupSchema); const Person = mongoose.model( 'Person' , new mongoose.Schema({ name: String , groupId: mongoose.ObjectId })); const g = await Group.create({ name: 'DS9 Characters' }); const people = await Person.create([ { name: 'Benjamin Sisko' , groupId: g._id }, { name: 'Kira Nerys' , groupId: g._id } ]); const group = await Group.findOne().lean().populate({ path: 'members' , options: { sort: { name: 1 } } }); group.members[ 0 ].name; group.members[ 1 ].name; group instanceof mongoose.Document; group.members[ 0 ] instanceof mongoose.Document; group.members[ 1 ] instanceof mongoose.Document;

If you're executing a query and sending the results without modification to, say, an Express response, you should use lean. In general, if you do not modify the query results and do not use custom getters, you should use lean() . If you modify the query results or rely on features like getters or transforms, you should not use lean() .

Below is an example of an Express route that is a good candidate for lean() . This route does not modify the person doc and doesn't rely on any Mongoose-specific functionality.

app.get( '/person/:id' , function ( req, res ) { Person.findOne({ _id: req.params.id }).lean(). then(person => res.json({ person })). catch (error => res.json({ error: error.message })); });

Below is an example of an Express route that should not use lean() . As a general rule of thumb, GET routes are good candidates for lean() in a RESTful API. On the other hand, PUT , POST , etc. routes generally should not use lean() .

app.put( '/person/:id' , function ( req, res ) { Person.findOne({ _id: req.params.id }). then(person => { assert.ok(person); Object .assign(person, req.body); return person.save(); }). then(person => res.json({ person })). catch (error => res.json({ error: error.message })); });

Remember that virtuals do not end up in lean() query results. Use the mongoose-lean-virtuals plugin to add virtuals to your lean query results.

Plugins

Using lean() bypasses all Mongoose features, including virtuals, getters/setters, and defaults. If you want to use these features with lean() , you need to use the corresponding plugin:

However, you need to keep in mind that Mongoose does not hydrate lean documents, so this will be a POJO in virtuals, getters, and default functions.