JavaScript Memory profiling for Backbone and React
Memory Profiling at Housing.com
Housing.com is a single page application made using Backbone.js and some parts of Marionette and React as well. Memory Leaks are of utmost importance to a single page application architecture as the tab is never closed and is at constant danger of leaking memory.
We have always pushed to ship products fast and have never really gotten about to profiling our website. This post is sort a guide for Frontend developers are Housing.
Step 1. Identify if you have a problem
So one of the best ways to do this is to open the task manager in Chrome and see the memory usage going back and forth in your application. If the memory goes back close to what it was when it was in the beginning, everything is fine.
Step 2. Take Heap Snapshot
Now there might be many entries and it would be very difficult to find out which ones are from current state and which objects are ghosts of a previous state. So now identifying that will depend on how you code. But one common aspect would be to find Detached DOM Trees. These are the major chunks of memory which you should not have. Though some of these might actually be in use, when you have kept a DOM node in memory to be added on some interaction. So look for where exactly it is referenced.
Step 3: Identify good Objects
Some class definitions of objects will forever persist through your website’s life. No need to be alarmed, they should have a small footprint and are used to create actual object instances. They may increase if you lazy load code files on user interaction.
Let’s take a look at some common SPA practices. Obviously the following is true only without code minification.
Standard Backbone Extend
var AppView = Backbone.View.extend({
el: '#app',
…
});
Coffeescript’s extends
class AppView extends Backbone.View
el: '#app'
…
React’s createClass
var AppComponent = React.createClass({
…
});
One common mistake to avoid in such class definitions is to have instances of other classes stored on keys. You can change this to initialize these sub objects when the actual object is instantiated. Though this can be intentional and is a choice for the developer.
var ModelA = Backbone.Model.extend({
…
});//Bad
var ModelB = Backbone.Model.extend({
defaults: {
a: new ModelA()
// Will never be garbage collected.
// All instances of ModelB will have reference to same ModelA
}
});//Good.
var ModelC = Backbone.Model.extend({
defaults: function () {
return {
a: new ModelA() //Good.
};
}
});
Step 4: Isolate Bad Objects
Now most of the times removing detached DOM nodes will give you the needed performance. Alternatively you can isolate the bad objects by searching for instances of objects which are not needed on the current page.
Standard Backbone extend
AV = Backbone.View.extend({
…
});var a_view = new AV();
All objects that you create from classes which were created using Backbone.X.extend will be clubbed under “child” tree. It would contain all objects whether they be Views, Models, Collections, Router or History. This would get quite confusing as you have to look through each object’s __proto__ to guess what the class of that object might be. This is because the constructor is stored in the “child” variable. [Github Source] [Annotated Source]
Though there is a solution. You can pass a named constructor function as a key in the argument to the extend function.
var AppView = Backbone.View.extend({
constructor: function AppView () {
return Backbone.View.apply(this, arguments);
},
…
})
Though it is cumbersome to write this function in every single extend call. You can also replace the extend function to this for you taking another __name__ parameter. [StackOverflow Source]
Coffeescript extends keyword
This is the easiest of all methods as all objects will be clubbed under the same name as defined in the class definition.
class AppView extends Backbone.View
el: '#app'
The above code will give the same result as passing the constructor function in the Backbone extend example.
React’s createClass
All instances will be under ReactElement. Kinda sucks doesn’t it. So far I haven’t found out a way of overriding this. There are various debugger tools for Backbone, but the React debugger only has support for modifying state and properties.
Step 5. Find references and get rid of them.
In the above example the reference of this view is retained as it has a event listener on the “destroy” event on some model. In essence you can access the object in the console as
window.app.todos._byId['7d905d94-d023-1ac8-6b33-0948ca6e11d1']
._events.destroy[0].ctx
So best way to get rid of this reference is to remove the listener. Similarly you can remove other references.
There can be other cases where the object may not be directly be accessible on some key of window but may be in context of some function. Nevertheless the Retainers Pane will mention that and you can take necessary steps to clear them.
Bonus Bug
So I was profiling our website over two pages and could not find the reference for detached DOM trees which were not being garbage collected. These trees were almost as big as the pages themselves. We use Coffeescript so my general method is to look for instances which should not be there on the current page instead of looking at detached DOMs. But this time all instances were being garbage collected. Everything except these Detached DOM trees whose references were 15 to 16 level deep in contexts.
After days of rattling, I found out that this was a bug with jQuery. We were using an old version on jQuery. The bug can be found documented on Github and StackOverflow.