Eventual Consistency

Avoiding Common Backbone.js Pitfalls

With the rise of HTML5 and the huge advancement in Javascript performance and APIs in modern browsers, single page applications are now more popular than ever. With that, several open source frameworks and libraries were born to help us develop these somewhat complex applications.

Backbone.js, while being one of the more popular choices, is also one of the smallest ones (in both terms of scope and actual byte count). So while Backbone is tremendously helpful, it does leave a lot of grunt work for us, the developers, to do ourselves.

This sometimes creates a bit of friction when getting started, as we face common problems, expecting Backbone to take care of them for us, when in reality we may leave "holes" or hidden bugs in our app. Here are some common ones, that are easily avoided, once we realize they exist.

1. Creating memory leaks by not unbinding events

A common pattern in Backbone.js is creating views that listen on changes in models or collections. This technique is usually intended to allow the view to automatically re-render itself when the underlying data changes. It also means that for large collections we may end up with many views (at least one for every model in the collection) that we may dynamically create or destroy based on changes to the data.

The problem arises when we remove a view (usually by calling its .remove() method), but forgetting to unbind the methods that listen on model changes. In such a case, even though our code may no longer hold a reference to that view, it is never garbage collected since the model still holds such a reference via the event handler.

Take this view for example:

var SomeModelView = Backbone.View.extend({
  initialize: function() {
    this.model.on('change', this.render, this);
  },
  render: function() {
    // render a template
  }
});

When calling the .remove() method, the "change" event handler (our render function) is still bound. So while the DOM element may be removed, the view object itself is never released from memory.

Solving this is easy (especially since Backbone 0.9.x) - all we need to do is to stop using .on() when binding the event handler. instead, we can use the new .listenTo() method, like this:

initialize: function() {
    this.listenTo(this.model, 'change', this.render);
}

The biggest difference here being the shift in responsibility from the model to the view. This means that whenever we call .remove(), the view will automatically unbind any event bound to it using the .listenTo() method, essentially fixing this common leak.

2. Causing multiple DOM reflows when rendering collections

Another common problem that is often visible with large collections is that on update or change, we render a view for every single model in the collection. While this is sometimes necessary, it can lead to severe performance issues and adversely affect UI responsiveness. Especially on old computers and mobile devices.

An example view:

var SomeCollectionView = Backbone.View.extend({
  initialize: function() {
    var self = this;
    this._views = [];
    // create a sub view for every model in the collection
    this.collection.each(function(model) {
      self._views.push(new SomeModelView({
        model: model
      }));
    });
  },
  render: function() {
    var self = this;
    this.$el.empty();
    // render each subview, appending to our root element
    _.each(this._views, function(subview) {
      self.$el.append(subview.render().el);
    });
  }
});

On a large enough collection, the performance hit will be visible even on modern browsers running on modern hardware. The reason for this is that every .append() we do in the render function causes the DOM to reflow - meaning that the browser has to recalculate the position and size of every element in the DOM tree. This is a relatively expensive operation, especially when multiplied by the amount of models we have in our collection

One way to avoid it, is to collect all rendered elements into one documentFragment, which is basically just a container for DOM elements, and then appending that single container to the DOM tree - triggering only a single page reflow.

Here is our render method again, this time with a single reflow:

render: function() {
  this.$el.empty();
  var container = document.createDocumentFragment();
  // render each subview, appending to our root element
  _.each(this._views, function(subview) {
    container.appendChild(subview.render().el)
  });
  this.$el.append(container);
}

3. Doing unnecessary XHR requests on page load

Backbone.js apps generally contain a single HTML page, that initializes some basic structure and loads a few js and css files to get the app up and running. Application data is retrieved using Collections and Models, using their .fetch() methods.

While this is generally good practice, it has one noticeable disadvantage: when loading the page, a user has to do at least one HTTP request to fetch our HTML page, and as soon as the app loads, our .fetch() methods make a couple more HTTP calls to fetch data.

This can lead to noticeable load times on slow networks.

If we serve our HTML page using a server side template, we can simply embed the JSON data needed to populate our initial models right in the page itself. It's not a pretty solution, but it may help in many cases.

It works in a very simple way: we add a <script> tag right before including our application's code to the page, with something similar to this:

<script>
var appInitialData = {{server_generated_json}};
</script>

Where server_generated_json is a JSON encoded string that our server generated before rendering the HTML. When initializing our collections and models, we can populate them with data from this object, saving us the need to do multiple .fetch()es and reducing the load time and network traffic used.

4. Non-optimistic AJAX

Unless we have some strict validation on the server that may return an error, AJAX updates don't really have to wait on a server response. Take this view code for example:

var SomeModelView = Backbone.View.extend({
  events: {
    'click .save-name': 'saveName'
  },

  saveName: function(e) {
    e.preventDefault();
    // get the new name from the input field
    var changedName = this.$('.name-input').val();
    // save it on the model
    this.model.save({name: changedName}, {
      success: function(model, response, options) {
        // render changes to the model
      },
      error: function(model, xhr, options) {
        // render an error message
      }
    });
  }

});

The obvious problem here is that the updated name is only rendered upon receiving a successful response from the server, via the success handler. Since in reality the operation will almost never fail on the server, there is no good reason not to render these changes immediately.

In the rare event when things do go wrong, we can "undo" the rendered changes and present an informative error message to the user.

5. Mixing concerns between Views and Models

Another problem that is usually related to such success callback functions, is that they define a certain behavior to take place when a model changes. If we use that callback only to change the way a model is rendered, then it's not such a big deal, but it's all too easy to simply slap more code in there, updating other attributes, or even changing other models. Such logic certainly does not belong within the scope of our view code.

It makes things much harder to debug, and much harder to maintain. Enforcing a good separation not only helps us understand our own code better, but it also makes us think about our design and understand the dependency between the components we use.


I'd be happy to hear your thoughts about this, and common pitfalls you have encountered yourselves. Leave a comment below or Follow me on Twitter to keep the discussion going.

comments powered by Disqus

Hi, I'm Oz Katz

I am a co-founder and CTO over at Swayy.

I usually write about software development using Python, JavaScript and other awesome, open source tools.

Feel free to reach out on Twitter, or contact me using the links at the bottom of the page.