Avoiding Memory Leaks in Backbone.js

Posted on

Hunting and eliminating memory leaks in Backbone.js

Backbone.js is a fairly minimal JavaScript framework when you compare it to its more full-featured cousins like Ember or Angular. It can be a rude suprise when Backbone suddenly leaves you on your own when you were expecting to have a helping hand. There are many gotchas surrounding sub-views for example that aren’t typically mentioned in the many TODO list examples out there.

It is very easy to find yourself in a situation where your views are leaking memory if you aren’t careful. Happily, with some recent additions to the framework and some good tools, we can identify these problems and patch them up.

The Setup

Let’s start with a model and a collection:

class Item extends Backbone.Model
  # ...

class Items extends Backbone.Collection
  model: Item

We want to show the items in a list with each item in its own view. Let’s create the list view:

class ListView extends Backbone.View
  className: 'list'
  tagName: 'ul'

  initialize: ->
    @collection.on 'reset', @addAll, this

  addOne: (item) ->
    view = new ItemView(model: item)
    @$el.append(view.render().el)

  addAll: ->
    @$el.empty()
    @collection.each(@addOne, this)

  render: ->
    @addAll()
    this

…and the item view:

class ItemView extends Backbone.View
  className: 'item'
  tagName: 'li'

  events:
    'click' : 'remove'

  render: ->
    @$el.html(@model.get('name'))
    this

We want our items to update themselves when they are renamed:

class ItemView extends Backbone.View
  # ...

  initialize: ->
    @model.on 'change:name', @updateName, this

  updateName: ->
    console.log 'updateName'
    @$el.html(@model.get('name'))

  # ...

When an item in the list is clicked, we want to remove it from the DOM (not from the underlying collection for this contrived example).

class Item View extends Backbone.View
  # ...

  events:
    'click' : 'remove'

  # ...

Here is a working jsfiddle example

Straightforward right? Well, it turns out we’ve just hit a pain point that many a Backboner has found before us: we’re leaking memory. While we remove our ItemViews from the DOM when they’re clicked, they’re still registered to the model’s change events and therefore can’t be garbage collected. Because the views can’t be collected, the DOM nodes they control can’t be collected either (they become ‘detached’). These views are now ‘zombies’.

There is a second problem. When the models change, the events will continue to fire on our zombie views, which can cause strange behaviour in our UI.

I’m going to take what I learned from Andrew Henderson in his post How to detect backbone memory leaks and apply it to our example.

Here, in our initial snapshot, we see 5 ItemViews have been allocated as expected:

5 ItemViews

After removing the first 3 items, I then rename all the items in the collection and we see that all 5 ItemViews still respond to the changes. In the second snapshot we also see 3 newly detached DOM nodes. This proves we’re leaking memory.

3 detached nodes

Backbone 0.9.9 to the rescue?

But wait you say, the new version of Backbone comes with .listenTo which will automagically unbind from event listeners and now we can go back to faceroll development mode. Sweet! Let’s check it out.

@collection.on 'reset', @addAll, this
@model.on 'change:name', @updateName, this

becomes:

@listenTo @collection, 'reset', @addAll
@listenTo @model, 'change:name', @updateName

See jsfiddle version 2

Running through Chrome again, the click events are now behaving as expected, we’ve taken care of our detached DOM nodes, and there are only 2 instances of ItemView now as we would expect. That’s pretty great for such an easy change, hats off to the backbone team for that one.

Let’s keep going though, if we now remove the ListView itself from the DOM we’ve got another problem:

ListView leaves garbage

We’ve removed the UL node from the DOM, but we’ve left the last 2 instances of ItemView zombified. They’re still responding to model events and their DOM nodes are detached.

My basic approach is that if a view creates another view, then it should be responsible for cleaning it up also. Let’s do that now. The plan is to fire an event on the ListView that ItemViews will listen for and call #remove on themselves.

class ListView extends Backbone.View
  # ...

  addOne: (item) ->
    view = new ItemView(model: item)

    # The item view will listen for the clean_up event on the list_view
    # and clean up after itself and remove itself from the DOM
    view.listenTo(this, 'clean_up', view.remove)

    @$el.append(view.render().el)
    # ...

  removeItemViews: ->
    # let the children know it's time to put away their toys.
    @trigger 'clean_up'
    # ...

  addAll: ->
    # clear out any existing ItemViews when resetting the collection
    @removeItemViews()
    # ...

  remove: ->
    # when the ListView is being removed, it must clean up it's children
    @removeItemViews()
    super()

Let’s check Chrome.

No more detached nodes No more item views

No more lingering event handlers, no detached nodes, and no instances of ItemViews remain.

All done.

Leaks: plugged. Zombie scourge: put to rest.

Final jsfiddle