Finding a balance between observers and template granularity?


#1

As I’ve been hearing more about React, Ive been beginning to question my own usage of Blaze.

So there are two ways I can render data into my templates, each which represents a pole of reactivity granularity:

Far Right

Very fine grained reactivity; lots of observers, no unnecessary template re-renders.

<template name="post">
  {{postTitle}}
  {{postScore}}
  {{postType}}
  {{postUser}}
  {{postHappy}}
  {{postSad}}
  {{postHungry}}
  {{postHasManyManyFields}}
  ...
</template>
//the helpers look like this
//each helper is rerun only if the relevant data changes
postScore = function(){
  var id = nonreactiveGetId();
  Posts.findOne(id, {fields:{score:1}})
}

As the app becomes bigger, we start to see a lot of helpers and each one has its own Posts.findOne... scoped to its own field. That’s a lot of observers.

Far Left

Reactivity at a coarse level; only one observer, tons of re-rendering stuff that doesn’t need to be rendered.

<template name="post">
  {{#with post}}
    {{postTitle}}
    {{postScore}}
    {{postType}}
    {{postUser}}
    {{postHappy}}
    {{postSad}}
    {{postHungry}}
    {{postHasManyManyFields}}
    ...
  {{/with}}
</template>
//the #with helper looks like this:
post = function(){
  var id = nonreactiveGetId()
  return Posts.findOne(id)
}

//the other look like this, with the appropriate changes made to each
//all helpers are rerun when ANY field changes.
postScore = function(){
  return this.score
}

Middle

For large apps, there could be 20 or 30 spots that have dynamic data. The middle approach would be to establish logical groupings of reactivity based on related fields. Instead of 20-30 observers, we’d have maybe 3-5 (depends on the data, ofc)

Are observers so cheap compared to re-rendering that we should almost always go for the fine grained reactivity, Far Right approach?

If you’ve faced this with Blaze, did you notice any big setbacks with having arbitrarily fine reactivity? At what point did it become better to deal with the unnecessary re-renderings of Blaze, than to have N live queries open?

As always, thanks for any help, correction, clarification, or comment!


#2

Thanks for bringing this up. I have been fighting with this ever since I began with Meteor and refactored my app many times with different combinations of granularity.

I can’t provide a benchmark or a final answer, but I can give my feeling:

  • The “Middle” way is too complex (from an architecture standpoint). It is very difficult to keep track of what is grouped and what is not.
  • The “Far Left” way looks good at first sight but is an optimization nightmare. For example, it looks good in your code above, because there is nothing costly in the postScore helpers. But in practice, it is very difficult, when your app evolves, both to remember and to constrain yourself not to put anything costly in those helpers.
  • The “Far Right” way, although it looks dubious, is surprisingly efficient. Yes, it has a cost. Not only do you pay for the observer, but you pay for the initial findOne, which I found to be twice as slow as the same one with reactive:false.

In the end, here is what I do:

  1. I use the “Far Right” way most of the time.
  2. When the “Far Right” way shows performance issues, I move to the “Far Left” way with this additional rule: don’t use helpers inside a #with (put everything in the context instead)(and beware of helpers in sub-templates!).

One remark about case 2: the “Far Right” way should show performance issues in very specific cases only. If I consider your blog example, it would probably become a problem when displaying hundreds of posts on the same page. Before moving to the “Far Left” way, there might be other optimization techniques to consider, such as limiting the number of posts per page.


#3

Interesting. I’ve been going the Middle with my subscriptions, which is a different topic. It just makes a lot of sense to me to put data which only appears together into the same subscription.

OK so I think a general rule of thumb:

It’s natural to use the Far Left approach when you’re iterating over a cursor which #each, because the data context is already the document in question. In a detail view, it seems to make more sense to use the Far Right method.

You mention point 2, to beware of sub-helpers. If you maximally fine-grain (far right) your view such that every possible invalidation point is scoped by a sub-template, then this won’t be an issue.


#4

@Steve what do you think about the following pattern:

Use #with to set the context reactively as ONLY an _id to prevent the
problem you mention in point 2. Then, one can just use this._id in the
helpers to get grained data via live queries.

Too many alternatives. Could set an instance reactive variable in created
callback within an autorun, but then you have to do the
Template.instance().id.get() in every helper; ugly.

Or you could, get the id in the helpers themselves. Intuitively, I like the
#with solution the most because it seems to cache the id value closer
(available in helper this) to where it will be frequently used, whereas
in the latter two solutions, one must look it up every time.


#5

That is what I do. This way I have all the reactive stuff in one block and I don’t have to watch out for what is happening upstream.


#6

well the idea is that if the id itself changes, you’re gonna have to re-render everything else anyways for typical data, in which case the #with only returning and observing an ID isn’t bad, but I guess from an organizational viewpoint I can see what you’re saying.

Thanks for your feedback on this thread! I’ve got a few ideas of what I’m gonna do now :smile:


#7

Based on https://github.com/meteor/meteor/wiki/Using-Blaze#fine-grained-updates-that-play-nicely-with-jquery, Blaze is smart enough to only update the DOM parts where data has changed. So you don’t have to think about this at all.

The new rendering model doesn’t replace a template when its data changes, it just replaces its parts, like text nodes and element attributes. This approach is not only more efficient, it means that DOM elements generally remain in place as long their parent elements and templates are not removed by an #if, an #each, or other conditional logic. Even if a template’s data context changes, or its parent’s data context, or an attribute on an enclosing element, the changes are reactively propagated to where they affect the DOM, rather than causing whole parts of the DOM to replaced as before. Structural changes to the DOM or template hierarchy are only made when a clear reason exists, and you can expect critical elements such as text fields and videos to be automatically preserved.

Side note: I also wasn’t sure if Blaze is that smart, because when I started with Meteor the predecessor Spark was still around and it would rerender the whole template.


#8

@Sanjo I disagree. I’ve noticed that Blaze is decent at preventing bubble-up changes, but not so much at preventing out-in changes. It seems using {{#with document}}, if a single field in document changes, everything underneath will change. I changed a single array field in my object by pushing a new item onto it,but the whole page re-rendered, evident by random color helpers, which pick a random color to re-render in. Parts of the page that didn’t use the array were re-rendered.

The solution I have is to make maximum use of query projections to make sure there’s no unwanted helper and #with re-runs. This leads to excessive observers, though.


#9

@Sanjo, we were not talking about Blaze rendering. Blaze rendering is a specific issue, which comes with its own performance problems (example here).

Consider the following code:

{{#each post in allPosts}}
  <div>post.title</div>
  <div>post.body</div>
  {{#if costlyOperation}}
    <div>Hi!</div>
  {{/if}}
{{/with}}

Suppose allPosts returns 1000 documents. Now suppose one field of one post changes: Blaze is smart enough to only refresh the one corresponding DOM node, but costlyOperation will be called 1000 times anyway.


#10

I’m definitely not a Blaze expert, but reevaluating the template helper and updating the DOM are two different things. Because your helper returns a different value each time (random color), the DOM that uses the helper value will also change.


#11

I see. I think helpers should never be CPU expensive operations. You could use a ReactiveVar to cache the return value of your expensive calculation if that makes sense for your use case.


#12

Precisely, that was my point. In this thread we were discussing about performance issues caused by template helper evaluation (many small reactive source vs. one big reactive source).