When it comes to memory management and JavaScript applications using Backbone, you'll often hear about zombie views and how easy it is to unintentionally create memory leaks. The suggested solution to preventing memory leaks in Backbone applications most often comes down to using .listenTo() as opposed to .on() when setting up views that can respond to model and collection changes. Recently, since I have started working with Backbone again quite a bit at work, I wanted to learn how to find and verify memory leaks in a browser's developer tools so that I can be more prepared for when I am faced with an application that is having memory problems. Therefore, I set out to build an extremely simple Backbone page to try and answer my own questions.
- How can I identify memory leaks in a browser's developer tools?
- Does replacing the innerHTML of a collection view destroy model views and prevent zombie views?
- How can I measure and verify question 2?
This post provides my introductory exploration of finding memory leaks in Backbone applications using Chrome Developer Tools.
What is a Memory Leak?
As stated on the Chrome Developer Tools - JavaScript Profiling site,
"A memory leak is a gradual loss of available computer memory. It occurs when a program repeatedly fails to return memory it has obtained for temporary use. JavaScript web apps can often suffer from similar memory related issues that native applications do, such as leaks and bloat but they also have to deal with garbage collection pauses."
Memory Profiling with Simple Native JavaScript
Let's start off with a simple HTML page that loads Backbone and its dependencies for our later examples.
<!DOCTYPE html>
<html>
<head>
<title>Demo</title>
</head>
<body>
<div id="people-container"></div>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script src="https://jashkenas.github.io/underscore/underscore-min.js"></script>
<script src="https://jashkenas.github.io/backbone/backbone-min.js"></script>
<script>
// our code will go here
</script>
</body>
</html>
Take a Heap Snapshot by opening up the Profiles tab in Chrome Developer Tools.
Then, click on "Take Snapshot".
If you look at the list of HTML* constructors, nothing shows up for the HTMLLIElement constructor. This is because there aren't any li nodes on the page. If you've never heard of the HTMLXXX constructors such as HTMLLIElement, they are the constructor functions used to create various DOM nodes. Let's go ahead and create an li element and take a heap snapshot.
(function () {
var li = document.createElement('li');
li.innerText = 'Person 1';
document.querySelector('#people-container').appendChild(li);
})();
First off, yes we are putting an li element in a div without a ul or ol element. Later on this will be changing. Take another heap snapshot.
You'll notice that the object count for HTMLLIElement is 2. The first count is for the HTMLLIElement constructor function itself and the second is for the li instance that we just created saved to the variable li. Behind the scenes, document.createElement() is making use of the HTMLLIElement constructor and the factory pattern to create list item elements. The global document object has a reference to the li object so it cannot be garbage collected.
Now what happens when I replace the innerHTML of #people-container? Let's find out.
(function () {
var li = document.createElement('li');
li.innerText = 'Person 1';
document.querySelector('#people-container').appendChild(li);
document.querySelector('#people-container').innerHTML = '';
})();
By setting the innerHTML of #people-container to an empty string, the list item has been removed from the DOM and the li instance that we created has gone out of scope. The document object no longer has a reference to the li object we created so it was garbage collected. The 1 under Objects Count corresponds to the HTMLLIElement constructor that was used to initially create the li element.
Now what happens if we do the same exact thing as above without wrapping our code in an immediately invoked function expression (IIFE)?
var li = document.createElement('li');
li.innerText = 'Person 1';
document.querySelector('#people-container').appendChild(li);
document.querySelector('#people-container').innerHTML = '';
You will notice that we have an object count of 2 again. Why would there be an object count of 2 if the li we created was removed by setting the innerHTML of #people-container to an empty string? Even though we have removed the li element from the DOM, there is still a reference to our li variable on the window object since we created it as a global variable. Thus, the li object cannot be garbage collected. There's a few things we can take away from these examples.
"The garbage collector will not clean up global variables during the page's life cycle."
Also,
"If an object in memory is holding a reference to another object that you want garbage collected, this reference needs to be destroyed."
Memory Profiling with Backbone
Let's look at an example that is Backbone specific. We will set up the code so that we have a Backbone Collection of people rendered in a collection-view where each model in the collection has its own model-view. This is a very common Backbone scenario.
(function () {
var people = new Backbone.Collection([
{ id: 1, name: 'David' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'Sam' },
{ id: 4, name: 'Max' },
]);
// A Model View (an item view)
var PersonView = Backbone.View.extend({
tagName: 'li',
className: 'person',
render: function () {
console.log('rendering');
var html = this.model.get('id') + ' - ' + this.model.get('name');
this.$el.html(html);
},
});
// A Collection View
var PeopleView = Backbone.View.extend({
tagName: 'ul',
id: 'people',
render: function () {
this.collection.each(function (model) {
var view = new PersonView({
model: model,
});
view.render();
this.$el.append(view.el);
}, this);
},
});
// And to kick it all off ...
var peopleView = new PeopleView({
collection: people,
});
peopleView.render();
$('#people-container').append(peopleView.el);
})();
With this bit of code, we can see each person from our collection being rendered on the screen.
- 1 - David
- 2 - Jane
- 3 - Sam
- 4 - Max
We can also see 5 HTMLLIElement objects from our heap snapshot. 1 for the HTMLLIElement constructor and 4 li elements created from PersonView for each person rendered in our collection view.
Now let's modify #people-container and set its innerHTML to an empty string temporarily and take a heap snapshot.
$('#people-container').append(peopleView.el).html('');
As like before, removing the list items from the DOM by setting the innerHTML of #people-container to an empty string allowed the garbage collector to clean up all HTMLLIElement instances from memory because there were no references to these list item elements hanging around in our application.
Let's make 2 changes to our code. The first thing we are going to do is have our PersonView objects re-render whenever its model changes. We'll set this up using Backbone.Events.listenTo(). To find out more on the differences between .listenTo() and .on(), check out Managing Events As Relationships, Not Just References By Derick Bailey.
var PersonView = Backbone.View.extend({
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
tagName: 'li',
className: 'person',
render: function () {
console.log('rendering');
var html = this.model.get('id') + ' - ' + this.model.get('name');
this.$el.html(html);
},
});
I have set up this event binding in our initialize() method. The next thing I will do is store the people variable on the window object so that we can keep our Backbone Collection in memory. Many times in a Backbone application you will keep a collection around in memory so that you can do something with that data later on such as filtering it. It doesn't really matter where you store it. The key thing here is that it is still in memory somewhere and we have a way of accessing it.
// previous code here
// remove the li's from the page
$('#people-container').append(peopleView.el).html('');
// store off the people collection onto the window object
window.people = people;
Now let's take a heap snapshot.
What you'll notice now is that our list item elements are still being kept in memory and are not being garbage collected even though we have removed the list items from our page. Why is that?
Remember from before that if an object in memory is holding a reference to another object that you want garbage collected, this reference needs to be destroyed in order for the object to go out of scope and be cleaned up by the garbage collector. In this case, our PersonView objects are not being cleaned up. We are intentially keeping the people collection around by storing it on the window object. Each model in the collection has a reference to the corresponding PersonView, which has a reference to a corresponding list item element. We declared this relationship when we told our PersonView objects to re-render if its respective model changes.
this.listenTo(this.model, 'change', this.render);
Behind the scenes, a reference to the PersonView render method is being passed to the model. You can see this in the Backbone source:
var listenMethods = { listenTo: 'on', listenToOnce: 'once' };
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function (implementation, method) {
Events[method] = function (obj, name, callback) {
var listeningTo = this._listeningTo || (this._listeningTo = {});
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
listeningTo[id] = obj;
if (!callback && typeof name === 'object') callback = this;
obj[implementation](name, callback, this);
return this;
};
});
Because each model of our people collection in memory has a reference to the the view that rendered it, the browser cannot garbage collect these views. This is what is referred to as zombie views - views that stick around in memory when we think it has been gone and it comes back to haunt us and bring our application down.
How do we remove views correctly?
There are a few ways of doing this but what I will demonstrate is the simplest and most common way. Rather than just emptying the innerHTML which doesn't always allow for our views to be garbage collected, we should call a .remove() method on our views that Backbone.View provides. Backbone will unbind the view references from their respective models or collections to prevent our data from hanging on to view references which prevents our views from being garbage collected.
So, instead of this:
$('#people-container').append(peopleView.el).html('');
We will do this:
var PeopleView = Backbone.View.extend({
initialize: function () {
this.childViews = [];
},
tagName: 'ul',
id: 'people',
render: function () {
this.collection.each(function (model) {
var view = new PersonView({
model: model,
});
view.render();
this.childViews.push(view);
this.$el.append(view.el);
}, this);
},
});
var peopleView = new PeopleView({
collection: people,
});
peopleView.render();
// add the PeopleView collection-view to the DOM
$('#people-container').append(peopleView.el);
// remove each PersonView instance
peopleView.childViews.forEach(function (personView) {
personView.remove();
});
They key thing to note here is that in PeopleView.prototype.render(), we store off references of our PersonView model-views into a property called childViews. Then later on, rather than replacing the innerHTML of #people-container, we can iterate over all of the child views and call the remove method. Backbone will unbind each PersonView instance from its model before it is removed from the DOM, thus allowing our views to be garbage collected and freeing up memory.
Takeaways
- The garbage collector will not clean up global variables during a page's life cycle
- Make sure to remove all references to the objects that you want cleaned up by the garbage collector
- In Backbone, removing elements from the page by wiping out the innerHTML of the parent container element may not always destroy the individual views. Be sure to call .remove() on Backbone Views and this will unbind references from the models or collections that the views are listening to, assuming these event listeners were set up using .listenTo(). If you used .on() instead, you will need to manually unbind the view reference from the model or collection before calling .remove().
Conclusion
After reading Building Backbone Plugins by Derick Bailey and several of his articles on Zombie Views, I wanted to try this out myself while profiling the memory of my objects in Chrome Developer Tools. I highly recommend checking out his articles which I have posted below. Hopefully this has been a useful exploration in understanding memory leaks, particularly in Backbone applications, and profiling memory leaks in Chrome Developer Tools. If anyone has useful tips with regards to memory leaks that I have not discovered, please share in the comments! Thanks for reading!