Avoiding Memory Leaks in Backbone.js
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 ItemView
s have been allocated as expected:
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.
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:
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 ItemView
s 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 lingering event handlers, no detached nodes, and no instances of ItemView
s remain.
All done.
Leaks: plugged. Zombie scourge: put to rest.