Backbone.js 1.0.0 nested view memory leak in Chrome

I’ve played with Backbone.js recently and tried to figure out how to get rid of memory leaks caused by nested views. As a reference I used “How To: Detect Backbone Memory Leaks” post by Andrew Henderson.

After some research it looks like the proposed code for view disposal can be simplified when using with newer version of Backbone (which is 1.0.0 at the time of writing this post).

Backbone 0.9.2 code from original post:

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 
}

Proposed Backbone 1.0.0 code:

close: function() {
    console.log('Kill: ', this);

    this.remove();
}

As you can see, explicit unbinding of events is not necessary, if listenTo() is used instead of on(), because, according to library reference for remove(), it “Removes a view from the DOM, and calls stopListening to remove any bound events that the view has listenTo’d”.

But here is an interesting thing: if you try now to test for memory leaks, Chrome will still find leaking detached DOM elements in my example! See screenshot of Chrome heap snapshot after adding and removing several views:

dom-leak

The first easy – and wrong – fix was to explicitly remove references to DOM elements in subview’s close() method:

close: function() {
    console.log('Kill: ', this);

    this.remove();

    this.el = null; // ???
    this.$el = null; // ???
}

It worked. But why is it wrong? Because theoretically nothing holds reference to the removed view after this.remove() is executed, and it should be garbage collected with its detached DOM element by browser automatically. Thus setting el and $el to null should be redundant.

Lets figure out what makes the detached DOM elements stay in memory. According to heap snapshot analyzer the DOM elements are not removed, because parent Backbone views were not removed:

parent-view

It means that something holds reference to the removed views. Let’s check retain trees for our views:

view-retain-tree

There’s this strange _idToWrappedObject, which references the removed view. And it turns out (thanks to this SO answer), that it has something to do with Chrome’s cache for objects mentioned in console logs!

So the real fix is to clear console or remove the console.log() call from close():

close: function () {
    // console.log('Kill: ', this);

    this.remove();
}

Conclusion

1) Use tools to analyze memory leaks.
2) Dispose view with its DOM element using Backbone.View.remove().
3) Use Backbone.Events.listenTo() instead of Backbone.Events.on(), so that Backbone.View.remove() removes event handlers for you.
4) Don’t forget to remove all references to deleted views, so that garbage collector can free memory and detached DOM elements.
5) Console logging can cause caching of removed objects in Chrome and thus affect memory leak detection, so don’t forget to clear console before taking heap snapshot.

See full source code at http://plnkr.co/edit/xfJWIF.

Advertisements

One thought on “Backbone.js 1.0.0 nested view memory leak in Chrome

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s