If you have a desire to create sophisticated client-side Web apps, Backbone.js is an awesome place to start.
With client-side apps, a big area for concern is memory management. As front-end developers, memory management might not be something we’re all used to worrying about – however, when full page refreshes are few and far between, memory leaks can cause our app to come to a grinding halt.
In his article about zombie views, Derick Bailey did a nice job of explaining how Backbone views can remain in the JavaScript memory heap even after they are no longer part of the DOM.
Using methods such as remove() and unbind() within a custom close method of our views will get the clean up process started, however, there are a few more things I recommend doing in order to safeguard against memory leaks.
When you create a view in JavaScript, you’re creating DOM nodes and usually binding event listeners to them. When you remove these nodes from the DOM, their event listeners hold reference to them. As a result, the JavaScript engine will not Garbage Collect the nodes as long as there are references to them still in scope. I’ll give you an example.
Note: This was written a the time of Backbone 0.9.2, prior to the addition of listenTo().
Let’s start by creating a simple model.
var Model = Backbone.Model.extend({
defaults: {
text: 'Zombie'
}
});
Next, we’ll create a view for the model. Note the close function that removes the view from the DOM.
var View = Backbone.View.extend({
tagName: 'li',
className: 'zombie',
template: _.template('<%= text %>'),
initialize: function () {
this.model.on('change', this.render, this); // Event listener on model
this.options.parent.on('close:all', this.close, this); // Event listener on parent
},
events: {
'click': 'close'
},
render: function () {
this.$el.html( this.template( this.model.toJSON() ) );
return this;
},
close: function () {
console.log('Kill: ', this);
this.unbind(); // Unbind all local event bindings
this.remove(); // Remove view from DOM
}
});
Create and Application Level View.
var AppView = Backbone.View.extend({
el: '#app',
events: {
'click #add': 'addView',
'click #remove-all': 'closeAll'
},
addView: function () {
var model = new Model();
var view = new View({
model: model,
parent: this // A reference to the parent view
});
$('#bin').append(view.render().el);
},
closeAll: function () {
this.trigger('close:all');
}
});
Document Ready.
$(function() {
var appView = new AppView();
});
Basic HTML.
Zombie Generator 3000
See this code in action below.
Now let’s test to see if this code is prone to zombie views & memory leaks. Add a handful of views to the DOM, by clicking the “Add” button.
Open the console and select the Profiles tab. Select the option to take a heap snapshot and click the start button. This will take a snapshot of the memory heap. You will see “Snapshot 1″ appear in the left column.
Next, click the “Remove All” button. Take another heap snapshot by clicking the record button at the bottom left of the console. You will now have “Snapshot 1″ and “Snapshot 2″.
Select “Snapshot 2″ and click where you see the word “Summary” at the bottom of the console. Select the “Comparison” mode.
At the top of the console. Type into the “Class filter” field the word “Detached”. Here we are comparing “Snapshot 2″ with “Snapshot 1″ and filtering the differences for any “Detached DOM trees”.
Notice that the “#New” and “#Delta” columns both show the number of Detached DOM trees matches the number of views we appended to the DOM. If you drill into the Detached DOM trees you’ll see that the node is an HTMLLIElement, which is the very node we added.
Hover over the selection and a popover shows us that the className of this node is “zombie” – the very class of our views! This evidence shows us that we definitely have objects persisting in memory.
If you need further proof, switch over to the Console tab. The code was written to include logging when the view’s close function fires. You should already have some logging in there that reads “Kill: ” followed by the object, but now notice that if you click the “Remove All” button again, console logging continues.
In order to properly dispose of our views, we need to ensure that we remove all references to the view. Rewrite the close function of the view to read the following:
Note: New lines have been highlighted.
close: function () {
console.log('Kill: ', this);
this.unbind(); // Unbind all local event bindings
this.model.unbind( 'change', this.render, this ); // Unbind reference to the model
this.options.parent.unbind( 'close:all', this.close, this ); // Unbind reference to the parent view
this.remove(); // Remove view from DOM
delete this.$el; // Delete the jQuery wrapped object variable
delete this.el; // Delete the variable reference to this node
}
See this code in action below.
Run through the same tests again in the console and you should find that you no longer have Detached DOM trees or continued logging after removing your views.
Note: Just to be sure you’re not getting any false positives. Clear out your heap snapshots and empty the browser cache before running the tests again.
In closing, remember to remove any event bindings in your view if you’re using parent references or any type of Pub/Sub or Mediator patterns.
Thanks to Chris Novak and Corey Winkelman for their help researching and developing this solution.
For more on JavaScript Garbage Collection, check out Backbone.js and JavaScript Garbage Collection.



