How To: Detect Backbone Memory Leaks

by Andrew on November 4, 2012

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.

    • Mohsen1

      Nice! Thank you for sharing.

    • http://www.facebook.com/lewis.he.1 Lewis He

      Nice post.

      A few arguments:

      1. the experiment measures two inputs : one is the event binding on model, the other is the event binding on the view’s parent ‘UL’. However, it doesn’t point out which is the cause of the memory leak.

      analytically, the parent ‘ul’ has reference to the view even after the view called close() while the view’s corresponding model is eligible for garbage collection when it runs out of scope (in ‘addView’ function).

      that is to say, it is the parent ‘ul”s event binding cause the memory leak.

      2. of course the post provide a completed solution. However i just want to know more about the purpose of each of those ‘clean statements’:

      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

      and is there any duplicates or overkill? especially for the last 2 delete statements.

      • http://andrewhenderson.me/ Andrew Henderson

        Thanks for the comment, Lewis.

        1. You’re correct. It measures two bindings. My understanding is that it is the reference to the parent that is causing the memory leak, but any bindings to other objects should be cleaned up in the close function since they could potentially cause a leak. So it’s good measure to unbind both.

        Just to clarify, this.options.parent is a reference to var appView in Doc Ready which is an instance of AppView(); The element for this instance is div#app.

        2. Based on the modified comments you provided, I think you have a good understanding of the clean statements. None are duplicate or overkill because there are instances of each binding type in this view. As far as the last two lines, both are cached references to the DOM node, one jQuery wrapped, one not. It seems both need to be deleted in order to avoid Detached DOM trees.

        Let me know if this helps clarify. Happy to discuss further!

    • http://www.facebook.com/people/Greg-Funtusov/1681688047 Greg Funtusov

      Do you think the new view.listenTo(object, event, function) / view.stopListening() will simplify it? It should remove all the unbind statements.

      • http://andrewhenderson.me/ Andrew Henderson

        Yes, that was the Backbone team’s intent with adding that method. This article was written for v0.9.2. I haven’t run any memory leak tests since the listenTo method was added in v0.9.9.

        I recommend following the same steps outlined in this article, using listenTo, to see if you have any remaining detached DOM trees. That might be an indicator on how well a job Backbone’s remove method does at cleaning up.

        If you find it’s leaving things behind, you may want to use a custom close method that also deletes this.$el and this.el.

        Let me know what you find!

    • Divyesh

      Hello Andrew,

      Thank you for this useful information !!!
      I have created one small application using backbone, and now I am facing Memory Leak issues in it.

      I have used your above code and I have tried below things using Google Chrome’s Dev Tool,
      - Before adding multiple view’s, I took 1st snapshot of heap memory it was around 3.5MB
      - After adding multiple view’s, I took 2nd snapshot and size was around 10MB
      - After deleting all view’s, I took 3rd snapshot but still size was around 10MB

      My doubt is,
      - Why heap size did not decreased after deleting all view’s even though GC runned.

      Your comment on this will be very useful to me…

      • http://andrewhenderson.me/ Andrew Henderson

        Memory leaks can be hard to track down. A good test is to listen for a click event within the view and then write a console.log within the assigned function. Fire the click event and see the console.log. Then discard that view for another. Fire the click event on the new view. If you get two console.logs for that click event, it means the first view was held in memory. This is usually due to jQuery’s attached listener. If you’re using Backbone 0.9.9 or 1.0, make sure you’re using the .listenTo() method instead of .on().

        • Divyesh

          Thanks for your reply Andrew !!!
          I have used Backbone v0.9.10 and I have tried your suggestions…
          I think view is getting destroyed, as click event is not fired after removing the view.
          I have also checked that there is not a single detached DOM element after removing the view but still size of heap memory is increasing…

          • http://andrewhenderson.me/ Andrew Henderson

            Sure thing, Divyesh. Which tool are you using to determine the heap size? The graph within the timeline panel may show a higher number on the top left, but that is only because the timeline must maintain scale. It will always show the largest size the heap reaches during activity. If the heap decreases at the end of the timeline as it does in the attached photo, that means garbage collection is working.

            • Divyesh

              Found the reason for memory leak on heap…
              - I was creating hundreds of child view’s but I was not maintaining references to those childs, but now I am maintaining references and heap size goes down when deleting child views…

              What I learned !!!
              - Keep track of every child view, if exists
              - When parent view is getting destroyed call destroy of every child view, so has to write destroy of every view
              - Ensure every event binding gets unbound in destroy

              This much care is sufficient, isn’t it?

            • http://andrewhenderson.me/ Andrew Henderson

              That would do it!
              Those three steps are good practice.
              For the third you mention, I would encourage you to make use of Backbone’s listenTo() method which is the similar to on() but get unbound when calling remove() on the view. You won’t need a custom destroy function unless you’re creating bindings somewhere outside the view itself.

      • adrian

        GC often work in a “mark and sweep” fashion that is similar to the Trash folder procedure: 1. marking or moving files to the folder marked Trash; and 2. sweeping or deleting the files within the Trash directory. Step 1 is fast; step 2 is slow. Your available disk size will not change after step 1. Its plausible that your GC has only done the mark process, thus the heap size is no different.

    • http://twitter.com/igaidai4uk Igor G

      Great article, thanks.
      One thing to add, that might help, prefer to set null rather than using delete.
      Check the delete section: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml

      • Joseph

        Hello Igor,
        The use of delete is fine since we are deleting properties of an object. In the code block,
        this is actuallu pointing to the view instance object. $el and el are properties of it. So we can use delete to remove them. Even Google’s Style Guide say’s that.
        Joseph

        • Radko Dinev

          Hi Jospeh.
          In general using `delete` exposes a property from the prototype chain with the same name, if there is such.
          Moreover, as the Google Code style says without details, deleting is bad for performance; the reason is “hidden classes” (https://developers.google.com/v8/design#prop_access) and each time you change the structure of the object (number and type of properties) the JS engine (V8) has to do much more work.
          Assigning to `null` avoids those two.

          • http://andrewhenderson.me/ Andrew Henderson

            I agree, Radko. Using ‘null’ is a better approach.

    • yuri

      It looks like in Backbone 1.0.0 it’s enough to only call remove(). Please see my post for more info: http://metametadata.wordpress.com/2013/06/17/backbone-js-1-0-0-nested-view-memory-leak/

      • http://andrewhenderson.me/ Andrew Henderson

        You’re correct. As long as you are adding listeners through the view’s events object or Backbone’s new listenTo() method, these should be cleaned up when you call view.remove().

    Previous post:

    Next post: