HTML5 - Snagging Javscript Memory Leaks

Working on high-performance HTML5 applications, I have noticed that one of the most common anti-patterns is the ‘pinning’ of closures to the Visual Tree.

Pinning is a situation where your object graphs stay attached to the Document/Window scope for the lifetime of your HTML5 application


Google Chrome includes a Heap Profile Tool that is invaluable for tracking memory leaks

Example of a Memory Leak caused by Closure Pinning

Consider the following page, which creates a bunch of Components, attaches them to the Document Loaded event and then tries to delete their references:


 	// Component Definition
 	function Component(name) {
 	    this.pageName = name;
 	}
 	
 	
 	// Lets decorate the Component instances with
 	// a method that can do some work
 	Component.prototype.onLoaded = function() {
 	    // do some fancy work
 	}
 	
 	// On Component.attach, lets subscribe to the
 	// DOM's loaded event and do something on our
 	// self closure context
 	Component.prototype.attach = function() {
 	    var self = this;
 	    $(document).load(function() {
 	        self.onLoaded();
 	    });
 	}
 	
 	
 	// Test Case: Create a ton of Components
 	for (var i = 0; i <= 4; i++) {
 	    var newComponent = new Component('my page');
 	    newComponent.attach();
 	    delete newComponent;
 	    newComponent = null;
 	}​
 	

What Chrome Reports

image

Explanation

The above code creates a Component which is ‘attached’ to the page’s Document scope via the loaded event.

The key to this leak is the ‘self’ reference in the loaded callback closure.

In order for the window to be able to call the pages on ‘future’ loaded events, the Component instances have to be held by the Document. Hence, the Component instances are ”Pinned” to the Document and the Delete calls do nothing…

Even NULL’ing out the local reference doesn’t buy the Component it’s freedom to flee.

Another important observation is that each Component instance appears to be pretty big; this means that the Component and all of its direct references are hanging around in Memory in relation to the ‘pinned’ closure.

In Short:

Proving the Leak by Dereferencing the Self Closure Pinning

Let’s prove this hypothesis by removing the self closure reference, but leaving the loaded event handler

// On Component.attach, lets subscribe to the
 	// DOM's loaded event and do something on our
 	// self closure context
 	Component.prototype.attach = function() {
 	    var self = this;
 	    $(document).load(function() {
 	        // Self reference removed, not causing closure leak
 	        //self.onLoaded();
 	    });
 	}
 	

What Chrome Reports

image

Explanation

In this case, the loaded event is still registered and the Component instances can still receive updates from the Document. The difference is that the Document has no direct reference to the Components themselves, by virtue that the callback can perform without the Component remaning in memory.

We can then call Delete and newComponent=null on the knowledge holder and the relationship is severed.

In Short:

Avoiding the Closure Pinning Leak by Managing Relationships

Let’s take an alternative approach to view context Pinning by introducing a life-cycle to the Component Instance

// Component Definition
 	function Component(name) {
 	    this.pageName = name;
 	    this.onLoadedHandler = this.onLoadedHandler.bind(this);
 	}
 	
 	
 	// We can now do our work with the Component
 	// context passed as expected!
 	Component.prototype.onLoadedHandler = function() {
 	    var otherPageName = this.pageName + 'hi!';
 	}
 	
 	// On Component.attach, lets subscribe to the
 	// DOM's load event and pass our context via the
 	// Javascript apply method (you can also use .call)
 	Component.prototype.attach = function() {
 	    $(window).bind("load", this.onLoadedHandler);
 	}
 	
 	// Detach the load event handler from the document
 	// context for-realz
 	Component.prototype.detach = function() {
 	    $(window).unbind("load", this.onLoadedHandler);
 	}
 	
 	
 	// Test Case: Create a ton of Components
 	for (var i = 0; i <= 4; i++) {
 	    var newComponent = new Component('my page');
 	    newComponent.attach();
 	    newComponent.detach();
 	    delete newComponent;
 	    newComponent = null;
 	}​
 	

What Chrome Reports

image

Explanation

The three important additions to the javascript here is

  1. We have to bind the this context to the handler callback method (see the constructor)
  2. Using JQuery Bind api to observe the document event
  3. Introducing literal Attach and Detach phases to the life-time of the Component Instances

Thanks to Vyacheslav Egorov for pointing me in the right direction about the best way to pass the context to the handler.

These combinations allow us to avoid context Pinning to the Visual Tree through the abstraction of the native Javascript call-stack.

In Short:

Conclusion

Closures are awesome in that they are wicked-easy, especially when designing Asyncronous API’s.

For most historical web sites, closure leaks will be flushed out within a few minutes, swept away by the constant browser refreshes. But, for the HTML5 Architect, always keep track where your Business Layer is ‘Pinned’ to the Visual Tree. This could mean the life, or death, of your application in the long-run.

One dangling association could result in massive object graphs staying in memory for the life-time of the Document/Window state, which can be considered Application-scoped for long-term offline applications.