Resolving an inefficiency with Meteor.user()

Hi All,

I thought I might share with you how I resolved a serious client performance issue which may affect others. It only becomes apparent in large projects with a large or complex user document structure (both of which I have). Every time you have a Meteor.user() call in a reactive method and your user details change, Meteor will perform a full document comparison to ascertain if the change needs to be applied. With a complex user document this takes time. If you happen to have hundreds of different reactive calls to Meteor.user() it takes a lot of time. I, like most people I suspect, have my Meteor.user() calls scattered far and wide.

The issue becomes particularly apparent if you are on a screen that updates your own user document (say a preferences screen) because the UI reaction to the change can get really sluggish - in my case almost unusable. I suspect (though am not sure) the issue has something to do with this code in Meteors local_collection.js:

 findOne(selector, options = {}) {
    if (arguments.length === 0) {
      selector = {};
    }

    // NOTE: by setting limit 1 here, we end up using very inefficient
    // code that recomputes the whole query on each update. The upside is
    // that when you reactively depend on a findOne you only get
    // invalidated when the found object changes, not any object in the
    // collection. Most findOne will be by id, which has a fast path, so
    // this might not be a big deal. In most cases, invalidation causes
    // the called to re-query anyway, so this should be a net performance
    // improvement.
    options.limit = 1;

    return this.find(selector, options).fetch()[0];
  }

The basic problem is that Meteor.user() performs a Meteor.users.findOne() minimongo call with all the reactive baggage that carries. So I wrote the following and included it in my initialisation code:

if (Meteor.isClient) {
    const userCache=new ReactiveVar();

    //Resets user details when they change
    Tracker.autorun(() => userCache.set(Accounts.user()));

    // Replace the Meteor.user() method to grab our cached version rather then issuing a separate findOne()
    // Note that the options parameter is not supported for this specific case
    Meteor.user=() => userCache.get();
}

Now, Meteor.user() simply delivers (reactively) the contents of a reactiveVar and only the reactiveVar has a dependency on Meteor.users.findOne()

This completely solved my problem. If anyone else is experiencing client performance issues when updating your own user document this may be the answer.

5 Likes

Thatā€™s a pretty ingenious solution for the problem described, thank you for sharing it! Obviously this pattern can also be used for documents other than Meteor.user(), if repeatedly queried in many reactive computations.

But Iā€™m wondering how you ended up having a large or complex user document structure in the first place? Normally the user document contains just the bare minimum of additional properties, if any at all. All business data relating or belonging to a user is usually stored in various other collections, typically with a ā€œuserIdā€ as a property to denote tenancy / ownership.

1 Like

Certainly I have many collections that reference the user document with an Id, but where the data is essentially a 1-to-1 relationship with the user, thereā€™s not much point having another collection with the user id as essentially the key and putting the data there. I also have sophisticated user subscriptions where I need to call in other user documents that ensure only permitted user data is brought to the client. But for the logged in user themselves, I have a permanent subscription that includes pretty much all their data.

One to one explains the motivation; however, you might not need reactivity with all the data of any given user all the time. If the userā€™s data was split off from his user document to documents in other collections separated for each use case, you might have been able to subscribe every time only that portion that is actually needed for the given user for the given use case.

So for example if the large structure you have in the user document could be split into, say, 5 distinct units by use case, there may be cases when, on a certain page, the user might only need part 1, and on another page they may need part 2 and 4, and so on. In that case you wouldnā€™t have one relatively large subscription, but many smaller ones, tailored to the need on the page.

Sure, there are lots of ways and decisions to be made on how to structure your subscriptions, again made more complicated in my case because I have many pages that consist of many components (and sub components) that may require their own subscriptions. Anyway, this is going slightly off-topic. Just hope that this insight on Meteor.user() has been useful.

1 Like

Hi @dthwaite. I think youā€™d be interested in my pull-request which made it into Meteor 1.10:

Or see the section I added to the Meteor Guide for a summary: https://guide.meteor.com/accounts.html#prevent-unnecessary-data-retrival

This is also a performance problem for your server, because there are many Meteor core calls to Meteor.user() which fetch the full user object, e.g whenever a user logs in.

Using my new Accounts.config({defaultFieldSelector: {...}) option you can limit which user fields Meteor fetches internally.

My PR also added a field parameter to Meteor.user() so you can limit the returned fields in your own code. A trick I also use is to run this code on the client and server, to warn me where there are any calls to Meteor.user() without a field specifier:

const _origMeteorUser = Meteor.user;
Meteor.user = function(fields) {
	const userId = Meteor.userId();
	if (!userId) return null;
	if (typeof fields=='undefined' || fields===true) {
		console.log('Meteor.user() called without any field specifier');
		console.trace();
		return _origMeteorUser();
	}
	else if (typeof fields=='string') fields = {[fields]: 1};
	else if (fields.fields) fields = fields.fields;
	else if (!Array.isArray(fields)) return _origMeteorUser(fields);
	else {
		const obj = {};
		fields.forEach(f => obj[f]=1);
		fields = obj;
	}
	return Meteor.users.findOne(userId, {fields});
}

// This allows:

Meteor.user('emails');
Meteor.user(['emails', 'profile']);
Meteor.user({myBigArray: 0});

// But will show a warning message with:

Meteor.user(); // BAD!
1 Like

Thanks - yes, certainly this would help - and clearly you had experienced a problem. Though on the server no nasty document comparisons are going on to service the reactivity. If Iā€™d known this to grow into an issue then I might have wanted/thought of field selectivity in Meteor.user() as you have.

But, in practice, the issue is the document comparison in minimongo reactivity. If that did not happen then getting all the fields back would not be much of an issue quite frankly. So my solution is a 3-liner that sorts the problem universally. Itā€™s not really an option for me to go though many hundreds of Meteor.user()/currentUser calls and riskily working out what fields I will be using and what fields I wonā€™t.

Thanks for the further insight ā€¦

Actually, the solution is very easy you have just to write this line of code :

  const user = useTracker(() => {
        return Meteor.userId()
    });

And the best use of it, is to make it a global variable, and pass it through your router.