Blaze's reactivity is an incredibly nifty feature of Meteor. It does away with a huge amount of boilerplate code and is mostly really nice to work with.

However, nothing really comes for free, does it. The problem with reactivity is that unless you do some relatively awkward things, you may easily lose a lot of control over what gets called when in your own code: yes, I'm talking about Blaze's helpers.

Helper functions are reactive by design which means if you reference any reactive data source (such as minimongo cursor) in them, the helpers will be rerun anytime the underlying collection data changes to determine whether the UI should be updated. Speaking from experience, the helpers can run very often. Now, usually this is not a problem because computers have become pretty darn fast lately and the helper queries run in milliseconds so everything works just fine and UI is smooth.

Except sometimes it's not.

If you have a lot of data - and/or big documents - in your minimongo collection, re-runing the helpers can become a massive bottleneck because the minimongo query runs synchronously. Yes, "blocks CPU until finished" synchronously. How bad can it get? Well, let's see an example.

Slow Query

In this gist (the "slow query" section) I'm inserting 1000 documents (sample doc taken from https://www.reddit.com/.json) into an anonymous minimongo collection and then I run a simple query, sort the result using the sort operator and limit the result set to 50. Which is similar to what one could do for example when you want to show a list of posts for a user. Pretty usual stuff.

The results are pretty horrifying.

On my 2015 Macbook Pro the query runs on average in 150 ms. That's CPU blocked for over one eight of a second for a single run of the (hypotetical) helper. Which gets run every time you insert a new document into the collection. Not great, right? How can we do better?

Optimising Minimongo Query

After some experimenting it turns out that the slowest parts of the query is:

  1. Sorting the results (which is by Meteor's own admission "laughably inefficient").

  2. Retrieving the actual documents as JS objects (looks like minimongo does a lot of EJSON operations on the docs before returning them to you).

Fortunately, we can work around both of those: see the fetch optimised section of the gist. The key to making the query faster is a combination of:

  1. Avoiding minimongo's sort implementation in favor of our own (in this case underscore's sortBy).

  2. Using minimongo projection to limit what's returned in the result set.

Those two simple changes result in roughly ten-fold speedup in the query execution time (150 ms vs 16 ms on average).

Of course, if we use projection, the result set will not contain the full documents. However, minimongo has an implicit "index" on the _id field (which we already have) and therefore fetching the full document in a helper in a child template will be fast enough (cca 0.2 ms on average which means 0.2 * 50 = 10ms to fetch 50 documents one by one).

Overall, we've just made a speedup from 150 ms to 16 + 10 = 26 ms per query run. Pretty nice, right?

Happy reactive meteoring! I'm @tomas_brambora.