Behind the scenes of product and engineering at Quri.

Backbone.js Memory Management

Backbone.js is a lightweight JavaScript library that helps give structure to your frontend JavaScript code. In particular, your data is represented by Models and those models can be displayed with Views. While being able to decouple your code this way is great (as opposed to the jQuery selector/callback spaghetti code of olden days), it is easy to forget about memory considerations and properly clean up your views. In this post we’ll run through some examples of Backbone.js memory leaks, use Chrome profiler to help identify these leaks, and finally discuss ways to manage and clean up your views to prevent these leaks in the first place.

Examples of Backbone.js Memory Leaks

Lets say we have an index view and a “post” view for displaying a series of posts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BackboneMemoryExample.Views.Posts.IndexView extends Backbone.View
  template: JST["backbone/templates/posts/index"]

  initialize: () ->
    @options.posts.on('reset', @renderPosts)

  renderPosts: () =>
    @$('#posts').empty()
    @options.posts.each(@renderPost)

  renderPost: (post) =>
    view = new BackboneMemoryExample.Views.Posts.PostView({model : post})
    view.on('postWasClicked', @postWasClicked)
    @$('#posts').append(view.render().el)

  render: =>
    $(@el).html(@template(posts: @options.posts.toJSON() ))
    @renderPosts()
    this

  postWasClicked: (post) =>
    post.set('title', "Post Title #{Math.random()}")
    @renderPosts()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BackboneMemoryExample.Views.Posts.PostView extends Backbone.View
  template: JST["backbone/templates/posts/post"]

  events:
    "click" : "postClicked"

  initialize: ->
    @model.on('change', @attributesChanged)

  postClicked: ->
    @trigger 'postWasClicked', @model

  attributesChanged: =>
    @render()

  render: ->
    $(@el).html(@template(@model.toJSON()))
    console.log "post rendered"
    return this

In this contrived example, when you click on a post it updates it’s title with a random number, and all of the posts are redrawn. There are a few different types of events going on here. We have Backbone models and collections being bound to when updates are made (when a collection is ‘reset’, and when a model ‘changes’). We are binding to a Backbone view in the index page, and that ‘post’ view fires this event whenever it is clicked. Also, the Post View itself is binding to it’s own click event.

Here is what happens when we click on the Post view 10 times:

The expected outcome is only 11 log statements from the post render method (including the inital page load render). Yet because of a leak, it is being called in increasing amounts each click.

Normally you would not have a log statement like this in your render method, so this would have gone unnoticed. Using the Chrome profiler we can detect memory leaks and check the health of our application.

Using Chrome Profiler

Using the same example, lets look at ways to use Chrome Profiler to help us detect memory leaks.

In the Profiles tab in devtools, use the Take Heap Snapshop option to show how memory is being consumed by your application.

The initial memory consumption of the “posts” app is 2.4MB. I’ve loaded each post with a bunch of lorem ipsum text to help increase memory and help show changes as memory leaks.

After clicking the post multiple times between each snapshot, we can see memory growth:

`

When taking heap snapshots it is important to return to an initial state where you believe the memory should return to the base level (in this case 2.4mb).

Using the comparison option (located on the bottom bar), we can compare the last heap snapshot with an earlier one to help see what has changed. Digging in, we can see that the problem is in PostView.

Clean Up The Leaks

With Backbone.js (and any other javascript library), it is important to clean up after yourself. As in the previous example, memory can grow completely unnoticed. Not only that, but cpu cycles can be wasted because of bound function calls that are not cleaned up. The same code ends up running multiple times over and over. In this case it would be the render call, and it would just draw over itself multiple times completely unnoticed (except for the eventual slowdown as the app was used or left open).

Lets look at where we can clean up some events in the Post view.

Generally there are 3 types of events you want to clean up:

  • DOM Events (binding to onClick)
  • Binding to other Backbone.js Models and Collections
  • Binding to other Backbone.js Views

DOM events are easy and generally get cleaned up themselves as long as you remove the view (calling view.remove() delegates to jQuery’s .remove() and handles the cleaning up for you.).

For Backbone Models, Collections, and other Views, you’ll need to keep track of what you call .on on, and remember to call .off() when cleaning up.

Lets write a cleanup method in our Post view to help unbind from these events:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class BackboneMemoryExample.Views.Posts.PostView extends Backbone.View
  template: JST["backbone/templates/posts/post"]

  events:
    "click" : "postClicked"

  initialize: ->
    @model.on('change', @attributesChanged)

  postClicked: ->
    @trigger 'postWasClicked', @model

  attributesChanged: =>
    @render()

  render: ->
    $(@el).html(@template(@model.toJSON()))
    console.log "post rendered"
    return this

  leave: ->
    @model.off('change', @attributesChanged)
    @off()
    @remove()

And also add a little view management in our index page to help keep track of the subviews (named postViews in the code below) created and call leave on them when it is time to clean up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BackboneMemoryExample.Views.Posts.IndexView extends Backbone.View
  template: JST["backbone/templates/posts/index"]

  initialize: () ->
    @options.posts.on('reset', @renderPosts)
    @postViews = []

  renderPosts: () =>
    _.each @postViews, (postView) ->
      postView.leave()
    @postViews = []
    @$('#posts').empty()
    @options.posts.each(@renderPost)

  renderPost: (post) =>
    view = new BackboneMemoryExample.Views.Posts.PostView({model : post})
    view.on('postWasClicked', @postWasClicked)
    @$('#posts').append(view.render().el)
    @postViews.push(view)

  render: =>
    $(@el).html(@template(posts: @options.posts.toJSON() ))
    @renderPosts()
    this

  postWasClicked: (post) =>
    post.set('title', "Post Title #{Math.random()}")
    @renderPosts()

Now, clicking on the post multiple times, we get the expected outcome and the app runs much faster:

Backbone has also recently introduced two methods to help keep track of and clean up events: listenTo and stopListening.

Using listenTo, you can have the view itself keep track of all of these events, and then clean them all up with a simple call to stopListening:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class BackboneMemoryExample.Views.Posts.PostView extends Backbone.View
  template: JST["backbone/templates/posts/post"]

  events:
    "click" : "postClicked"

  initialize: ->
    @listenTo(@model, 'change', @attributesChanged)

  postClicked: ->
    @trigger 'postWasClicked', @model

  attributesChanged: =>
    @render()

  render: ->
    $(@el).html(@template(@model.toJSON()))
    console.log "post rendered"
    return this

  leave: ->
    @stopListening()
    @off()
    @remove()

These are just some basic things to keep an eye out for when using Backbone.js. Remember to profile your JavaScript regularly to help track down these types of memory leaks and keep your app running smoothly.

Comments