Speed
So, my assumption would be that aggregates are faster, always.
Benchmarks below are ran on the following schemas:
const personSchema = new Schema({
name: String,
})
personSchema.virtual('contracts', {
ref: 'Contract',
localField: '_id',
foreignField: 'personId',
})
const contractSchema = new Schema({
personId: mongoose.Schema.Types.ObjectId,
number: String,
amount: Number,
})
// contractSchema.index({ personId: 1 });


Aggregating with a lookup stage quickly becomes a lot slower compared to populate when no indexes are applied to the foreign id. At 5000 records, lookup becomes unbearably slow, while populate still manages to perform reasonably well


However, when indexes are applied to the foreign key, lookup becomes consistently faster.
Simplicity or Power
In summary, aggregate is faster when the collections are indexed properly. For quick prototyping or bad indexes, populate is better.