Quantcast
Channel: 懒得折腾
Viewing all articles
Browse latest Browse all 764

Improving the Performance of your Meteor JS projects

$
0
0

Improving the Performance of your Meteor JS projects

So you found Meteor, learned it in a day and started your next million dollar idea … only to find your new masterpiece is slow as all get out. What happened? Well, the reactive nature of the framework needs to be used wisely. (You don’t see Luke Skywalker using the force everywhere he goes do you? The force is cool, but he still does a few things the traditional way.)

Overuse of Sessions

When I first started exploring Meteor JS with the perspective of a PHP developer, I immediately applied my traditional thinking to Meteor’s session: that they are a place to store data over time. Well kind of…

Understanding Meteor JS Sessions

Meteor’s session doesn’t stick after a user initiates a browser refresh. In PHP, you’d expect the session information to persist a browser refresh, so right off the bat, Meteor’s sessions are working differently from PHP.

Instead, the purpose of Meteor’s session is to provide reactivity across your application and to keep your application’s state when the server initiates a refresh (when server code changes, CSS is updated, etc). See http://docs.meteor.com/#session for all of the details.

Meteor JS Sessions: You’re Doing it Wrong

I noticed developers using sessions as global variables when they couldn’t find the value they were looking for in a given context. A common example was trying to pass IDs of documents in Mongo around the application so it was easily accessed in multiple functions scattered about the app.

You should only be using a session variable when you need to set up some reactivity on it. Ask yourself, “When I set this session variable, do I need something else in the app to change?” If not, there is probably a better way to solve your problem.

Don’t forget about normal Javascript variables. These work just like they always did and using these normal variables often work perfectly instead of a session variable when reactivity is not needed. Don’t forget about the basics.

Fine tune your Meteor collection.find() queries to get only the data you need

I shouldn’t even need to write this section, because we’ve all been doing this with relational databases as well, right? We’d never do something like “SELECT * FROM …” riiiiight??

Make sure not to ask for more data than you need; it’s even more important in Meteor! Let’s pretend we have a Collection that stores documents about employees in a company. Of course you’ll have an employee_id, a first name, a last name, and so on. In Meteor, you might use this somewhere in a helper function like this:

1
var employees = Employees.find().fetch();

There is nothing wrong with this in itself, but what if for the work I need to do, I only need a list of employee ids, not all the other meta data, like name, address, etc? Well, in Meteor this is pretty important. If any code elsewhere in your app updates the name of an employee, even though you don’t care (in this specific case), the reactive template that uses Employees.find().fetch() will retrigger anyway, the DOM will be updated with the same HTML that was there before. How useless and damaging to your application’s performance!

What if you instead asked for only what you need?

1
var employee_ids = Employees.find({}, {fields: {_id: 1}}).fetch();

This says, “Please give me only the IDs of all the employees in the Employees Collection.” In contrast to the first query above, this will return only the data we need, something like [{_id: 3345}, {_id: 3567}, {_id: 125}, {_id: 9876}], a list of documents containing only the employee ids.

If other data in the employee collection is updated, like Name, it won’t cause your fine tuned query to rerun because if never looked at Name, saving browser cpu cycles.

Making your queries non-reactive

When you using your Collections, think about how that data is going to be used. If you can determine that you never need to update the DOM or HTML if the document(s) change, then you can tell Meteor not to make the query reactive. Then if data in the return documents changes, it won’t cause your template to rebuild.

It’ll take a little experience to get comfortable making this choice, but if you just keep asking yourself, “Does this find() need to rebuild a template if data changes?,” over time you’ll start to see places where you can turn off the reactivity without compromising your app.

To make a query non reactive, simply pass { reactive: false } as an option.

1
var employees = Employees.find({}, {reactive: false}).fetch();

Breaking down your templates

Try to make your templates as small as possible. Meteor restricts reactivity to as small a space as it can. If you have large templates with complicated nested loops, it’s possible small data changes can force large templates to rebuild.

Using {#isolate} for larger templates

If you do find yourself in a situation where you do have a large template, you can break it up using the {#isolate} tag. Meteor will attempt to isolate the changes to only that area so that the entire template is not reprocessed. See: http://docs.meteor.com/#isolate

Tips for Debugging Meteor JS:

Which templates are being reprocessed?

“Arg?! Where do I even start; I don’t even know what is being regenerated!”

I’ve had this same issue time and time again. I started going into the code and adding console.log() statements (all over the place). Needless to say, that was painful!

Eventually I got smart and made a post on Stack Overflow – http://stackoverflow.com/questions/15422456/detecting-which-reactive-query-was-triggered, luckily cmather had some insightful help. If you include the following code in your client code, and execute the function logRenders() *AFTER* all the other client code has been processed, you’ll get a nice view of which templates are called, in what order, and how many times.

1
2
3
4
5
6
7
8
9
10
11
function logRenders () {
    _.each(Template, function (template, name) {
      var oldRender = template.rendered;
      var counter = 0;
      template.rendered = function () {
        console.log(name, "render count: ", ++counter);
        oldRender && oldRender.apply(this, arguments);
      };
    });
  }

You’ll probably notice that if your application is experiencing problems, the same templates are being called over and over, and/or templates that shouldn’t be triggered at all are being retriggered. If your application updates the DOM and HTML needlessly, it slows down your app.

Tracking a specific Collection.find(), and when it reruns.

If you have a specific find() on a Collection that is giving you issue, take a look at http://docs.meteor.com/#observe or http://docs.meteor.com/#observe_changes. These two functions allow you to create a callback on the find() statement so you can see exactly when it’s retriggered. The callback will provide you the original object, the changed object, and even a diff of the object. This will let you track what is making a particular collection.find() to reprocess.

Here is some code that will add observe changes to all your find statements to help track which collection is causing a reactive change, also taken from http://stackoverflow.com/questions/15422456/detecting-which-reactive-query-was-triggered:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var wrappedFind = Meteor.Collection.prototype.find;
Meteor.Collection.prototype.find = function () {
  var cursor = wrappedFind.apply(this, arguments);
  var collectionName = this._name;
  cursor.observeChanges({
    added: function (id, fields) {
      console.log(collectionName, 'added', id, fields);
    },
    changed: function (id, fields) {
      console.log(collectionName, 'changed', id, fields);
    },
    movedBefore: function (id, before) {
      console.log(collectionName, 'movedBefore', id, before);
    },
    removed: function (id) {
      console.log(collectionName, 'removed', id);
    }
  });
  return cursor;
};

Final sanity check

When I first started noticing performance problems in Meteor JS, it was hard to get going. I wouldn’t see a lot on the page changing, but everything was still very slow. So intuitively I figured the DOM and HTML was being updated in places it shouldn’t be, and updating it to what was there before: pointless. So I took a an odd approach for lack of a better one: I opened the DOM inspector in the browser, and updated the DOM manually through the browser. I then went to my app, and made a change that shouldn’t effect the manually updated HTML I made in the browser. If my manual update gets reverted, I know I have some code that is overreaching its targeted area. If the manual change persists, it means your code is at least not updating the area you changed, which is a good start.
(Anyone with a better idea, comments very much welcome!)

If you have an idea, please let me know!



Viewing all articles
Browse latest Browse all 764

Trending Articles