Faster Mongoose Queries With Lean

    By default, Mongoose queries return an instance of the . 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.

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

    1. const schema = new mongoose.Schema({ name: String });
    2. const MyModel = mongoose.model('Test', schema);
    3. await MyModel.create({ name: 'test' });
    4. // Module that estimates the size of an object in memory
    5. const sizeof = require('object-sizeof');
    6. const normalDoc = await MyModel.findOne();
    7. // To enable the `lean` option for a query, use the `lean()` function.
    8. const leanDoc = await MyModel.findOne().lean();
    9. sizeof(normalDoc); // approximately 600
    10. sizeof(leanDoc); // 36, more than 10x smaller!
    11. // In case you were wondering, the JSON form of a Mongoose doc is the same
    12. // as the POJO. This additional memory only affects how much memory your
    13. // Node.js process uses, not how much data is sent over the network.
    14. JSON.stringify(normalDoc).length === JSON.stringify(leanDoc.length); // true

    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.

    1. const normalDoc = await MyModel.findOne();
    2. const leanDoc = await MyModel.findOne().lean();
    3. normalDoc instanceof mongoose.Document; // true
    4. normalDoc.constructor.name; // 'model'
    5. leanDoc instanceof mongoose.Document; // false
    6. leanDoc.constructor.name; // 'Object'
    • 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.

    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.

    1. const Group = mongoose.model('Group', new mongoose.Schema({
    2. name: String,
    3. members: [{ type: mongoose.ObjectId, ref: 'Person' }]
    4. }));
    5. name: String
    6. }));
    7. // Initialize data
    8. const people = await Person.create([
    9. { name: 'Benjamin Sisko' },
    10. { name: 'Kira Nerys' }
    11. ]);
    12. await Group.create({
    13. name: 'Star Trek: Deep Space Nine Characters',
    14. members: people.map(p => p._id)
    15. });
    16. // Execute a lean query
    17. const group = await Group.findOne().lean().populate('members');
    18. group.members[0].name; // 'Benjamin Sisko'
    19. group.members[1].name; // 'Kira Nerys'
    20. // Both the `group` and the populated `members` are lean.
    21. group instanceof mongoose.Document; // false
    22. group.members[0] instanceof mongoose.Document; // false
    23. group.members[1] instanceof mongoose.Document; // false

    also works with lean.

    1. // Create models
    2. const groupSchema = new mongoose.Schema({ name: String });
    3. groupSchema.virtual('members', {
    4. ref: 'Person',
    5. localField: '_id',
    6. foreignField: 'groupId'
    7. });
    8. const Group = mongoose.model('Group', groupSchema);
    9. const Person = mongoose.model('Person', new mongoose.Schema({
    10. name: String,
    11. groupId: mongoose.ObjectId
    12. }));
    13. const g = await Group.create({ name: 'DS9 Characters' });
    14. const people = await Person.create([
    15. { name: 'Benjamin Sisko', groupId: g._id },
    16. { name: 'Kira Nerys', groupId: g._id }
    17. ]);
    18. // Execute a lean query
    19. path: 'members',
    20. options: { sort: { name: 1 } }
    21. });
    22. group.members[0].name; // 'Benjamin Sisko'
    23. group.members[1].name; // 'Kira Nerys'
    24. // Both the `group` and the populated `members` are lean.
    25. group instanceof mongoose.Document; // false
    26. group.members[0] instanceof mongoose.Document; // false
    27. group.members[1] instanceof mongoose.Document; // false

    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.

    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 . On the other hand, PUT, POST, etc. routes generally should not use lean().

    1. // This route should **not** use `lean()`, because lean means no `save()`.
    2. app.put('/person/:id', function(req, res) {
    3. Person.findOne({ _id: req.params.id }).
    4. then(person => {
    5. assert.ok(person);
    6. Object.assign(person, req.body);
    7. return person.save();
    8. }).
    9. then(person => res.json({ person })).
    10. catch(error => res.json({ error: error.message }));
    11. });

    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.

    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.

    1. const schema = new Schema({ name: String });
    2. schema.plugin(require('mongoose-lean-virtuals'));
    3. schema.virtual('lowercase', function() {
    4. this instanceof mongoose.Document; // false
    5. this.name; // Works
    6. this.get('name'); // Crashes because `this` is not a Mongoose document.