Memory Leaks (Zombies!) With Event-Driven Code

Aug 17, 2012 at 5:28 PM
Edited Aug 17, 2012 at 5:35 PM

Any time we have an event listener (event handler, bind to an event, whatever you want to call it), we have the potential for a memory leak. This is due to the way the observer pattern works, which is the basis for all event handling systems (in .NET, Java, JavaScript, C++, Ruby or anywhere else). 

Let's say ObjectA can dispatch events (because it uses the WinJS.Utilities.eventMixin). If ObjectB wants to listen to those events, we call addEventListener on ObjectA:

  ObjectA.addEventListener("some event", ObjectB.SomeFunction);

This sets up the "SomeFunction" on ObjectB as the event handler. 

Let's also say ObjectA is a globally accessible object. It might hang off a namespace or be directly available in the global context. Either way, it hangs around all the time.

And now lets say it's time for the page that is holding on to ObjectB to go away. When this page is closed, the page's reference to ObjectB will no longer be needed. JavaScript's garbage collection should be able to clean up ObjectB.

But there's a problem. ObjectA is holding a direct reference to ObjectB through it's event listeners. Because of this, ObjectB will not be garbage collected. It will hang around in memory, and any time ObjectA triggers the "some event" event, ObjectB will get called - longer after the page that created ObjectB is closed. We basically have a zombie object (memory leak) at this point.

This is a very simple scenario that is very common in event-driven applications. It's also very easy to solve. When the page that created ObjectB closes, it needs to have ObjectB removed from ObjectA's event handlers:

  ObjectA.removeEventListener("some event", ObjectB.SomeFunction);

By doing this, we are telling ObjectA to remove the reference to ObjectB that it holds. Thus, when the page closes, no more references to the ObjectB will exist and ObjectB can be garbage collected. We have effectively taken the zombie's head off it's body, and killed it. :)

I've written about this extensively in JavaScript and Backbone.js, and have created a utility that makes this 100x easier to manage (there are some nuances to this, regarding anonymous callback functions and bound functions as event handlers that make it a bit tricky):

In spite of these posts and this code being specific to Backbone, the same principles and patterns apply to WinJS objects, and DOM events. We need to be sure our event handlers are not creating zombies in our app. Its also likely that we will want to create something like my EventBinder for WinJS (though we can certainly do things without it - it just makes things easier).

Aug 17, 2012 at 7:51 PM

FYI  - in my testing of our app so far, I'm not seeing any zombies yet. I've only tested the Rotate page so far, but that's a pretty standard example of how we built things, so I'm hopeful for the rest of the app.

Here's the explanation of why the rotate page doesn't have any memory leaks, even though we are not using `removeEventListener` anywhere in the app right now:

When the page loads up, it is handed a few things from the previous page: a query object and a selected item index. It uses these objects to load the image that it needs to display. Neither of these objects triggers any events, therefore they do not hold any reference to anything on our page. Our page is holding a reference to them. That means when our page goes out of scope, the references can be cleaned up. The previous page that owns the original reference to the query object is sitting in the same situation. When that previous page goes out of scope or lets the query object reference go out of scope, the query object is cleaned up.

The rotate page does make use of DOM events and events from our controller objects, though. When the page loads, it queries the DOM for a few elements and then attaches a few click events to them. The object that listens to the events from the DOM also triggers its own events. The page has a controller object that it creates, which listens to the events that our other controller objects have triggered. All of this has the potential to create a large number of zombies - DOM events handled by objects which trigger other events that are handled by objects. 

In this case, though, the DOM events all come from DOM elements that are contained within the page fragment. The page fragment is the portion of the .html page that the WinJS framework dynamically loads and renders when we navigate between pages. It's the HTML of the "single page" in the term "Single Page App", basically. Because of this, the DOM events that we are binding to are attached to DOM elements that will be removed when we navigate to another page (using the "Back" button, for example). When we navigate to another page, the page control object (the "code behind" for the page) also falls out of scope - and it's the the page control that created all of the objects we need for the page, did the DOM querying and got everything set up.

All of this means that when we navigate to a different page in the app, the DOM elements we are bound to and the page control that holds references to our objects both fall out of scope. Since our controller objects and their event bindings are managed in side of the DOM and page control that just fell out of scope, all of our objects are also falling out of scope. This means everything we created on this page is now garbage, out of scope, and able to be cleaned up the the JavaScript garbage collector. Therefore, the next time we navigate to the same page, even to load and manipulate the same image, we are creating a new instance of the controllers and DOM elements, without any reference to the previous elements or objects, hence no zombies and no memory leaks.

Even if this is the case and the app doesn't have any memory leaks, the information that I've shown here needs to show up in our code and in the documentation to make sure people understand what's going on with memory management in JavaScript.

If we decide to change our app design to use a common header and footer, for example, throughout all of the pages we may end up in a situation that needs to be explicitly handled. If we put a DOM element in the default.html page, outside of the fragment that gets swapped out on navigation, then any DOM events we wire up to those elements outside the page fragment will need to be cleaned up.

For now, though, we should be fine - at least for most of our pages. We should go through the rest of the pages with a fine tooth comb to ensure the rest of the app is zombie free, though.