Sunday, August 5, 2012

JavaScript memory leaks

"What? Memory leaks? In JavaScript?!?"

Yes.

Modern browsers nowadays do have garbage collectors that can successfully collect JavaScript structures with circular references. But let's see what could actually create memory leaks then.

The anatomy of a leak


As with all GC'ed languages, memory leak problems can appear at the interface with another language. Any ideas yet?

Well, in modern browsers, the JavaScript world and the DOM world are different languages and therefore, they are GC'd in different ways. So when you make references between JavaScript and HTML elements, you actually create a reference between these two worlds. Now, because these two worlds are different, there are problems when we come to detecting whether things are referenced or not.

While it is possible to detect that JavaScript objects or DOM elements are not referenced anymore by anything, detecting that there's a circular reference between JavaScript and the DOM is problematic. As of the time of writing, for some reason, none of the browsers out there can do that.

(EDIT: Nope, I was wrong. When I tested these cases in isolation in Chrome, they did not leak. But, I'm pretty sure I have seen at least one of these patterns leak in Chrome in a more complicated scenario. So it might be that the patterns discussed here - in this simple form - are somehow optimized by the compiler, which prevents them to leak. Or I am entirely wrong about Chrome, and it does cover for all these patterns gracefully.)

So, with no further ado, I would like to introduce you to...

Leak Pattern #1 (The Leaking Closure)


function callMeAndIWillLeak() {
  var div = document.createElement("div");
  div.onclick = function () {
    div.innerText = "Hello, leak!";
  };
}


This is the most obvious leak possible. The created DOM element references an anonymous function, which references div, which references the DOM element.

So DOM element -> (anonymous function) -> div -> DOM element.

The problem is that the anonymous function is a closure, which in JavaScript terms, means that it contains references to otherwise local variables (in this case, the variable div). The takeaway is that you should be very careful when giving closures to the DOM. Also, it is a good idea to use a battle-tested event system like JQuery's, which works around these leaks.

Leak Pattern #2 (The Leaking Inline Event Handler)


You might be surprised that older browsers (Ahem! You know who you are!!) leak in other situations too.

function callMeAndIWillLeak() {
  var div = document.createElement("<div onclick='foo()'>");

}

Yup, that's it! The function foo doesn't even have to exist!
At least, this doesn't affect modern browsers, I believe. Though I wouldn't be too surprised if I saw IE8 still reproducing this.

Leak Pattern #3 (The Leaking Expando)


Expando properties are additional JavaScript properties that you assign to DOM elements. Here is an example of a leak:


function callMeAndIWillLeak() {
  var div = document.createElement("div");
  var myObjectWichReferencesDiv = {
    /* ... */
    myDiv: div,
    /* ... */
  };
  div.myCustomProperty = myObjectWhichReferencesDiv;
}


This leaks due to the following circular reference: DOM element -> expando property -> complicated object -> DOM element.

Bonus. Leak Pattern #4 (The Leaking Spaghetti)


In practice, the leaks above won't be really sitting all that obviously, though. In most cases you will have no idea why your web page keeps growing in memory.

So in general, I have found that if I keep very well track of where I reference the DOM elements (especially the ones created programatically) and if I clear out with null when I'm done with them, I get away with it.

You can think of DOM elements as the only things in JavaScript that are not automatically garbage collected and which need manual clearing of all direct references to it - just to make sure.

Leak patterns summary

DO use a good event system (like JQuery's) to attach events.
AVOID using closure variables in event handlers.
DON'T use inline event handlers (<div onclick='foo()'>).
AVOID using expando properties.
If you do use expando properties, AVOID using non-primitive JavaScript types.
And if you do use expando properties with non-primitive objects, DO nullify those properties when they are not needed anymore.
DO nullify all direct references to DOM elements when not needed anymore, just to make sure.

Finding leaks


Now that I talked about what a leak is, we might as well look at how to find them.
I will show you how to use Chrome's developer tools for this one, although other browsers have similar features too.

EDIT: This assumes that you are able to reproduce the leak in Chrome.

The leaking action


First, use Chrome's task manager (Shift+Esc) to look at the per-tab memory usage. While keeping an eye on the memory usage try to find the user action which triggers an increase of memory every time you do it.

Alternatively, open Chrome's developers tools -> Timeline -> Memory and then click the record button. That will give you a more detailed view of where the memory gets allocated.

You will notice that the memory shown in the task manager is different from the memory shown in the timeline. This is because a lot of the memory is kept natively and is not shown in the timeline. Canvas or image data are such examples. When hardware acceleration is used, your web page could occupy significant amounts of memory in the GPU Process (shown separately in Chrome's task manager) as well. I'm guessing this memory comes from canvas and image data, mostly, as well.

Pinpointing the leaking objects


After you know which action leaks, go to Chrome's developer tools -> Profiles. And use the Take Heap Snapshot command. Take a snapshot before and after the action. You should return to the same state as before the action before taking the "after" snapshot on your web page (i.e.: the state in which you would normally expect the memory usage to drop to the initial value). After that, click on Snapshot 2 and in the bottom, where it says Summary, select Comparison.

That will show you all the objects that were created or garbage collected between the two snapshots. Note that not all the objects shown there are memory leaks. Some objects may have been replaced. Those will show up as well, since they are different objects now.

Note that Chrome does a GC just before taking the snapshot, in order to get rid of all the objects that would be GC'd anyway.

As we have seen, the most common source of leak are DOM elements. So a good place to start is looking for "Detached DOM tree" entries. Look through their properties and try to find out where they come from.



The memory leak is usually noticeable when there are "Detached DOM tree" entries of the order of hundreds. But if you are leaking images or canvases, then the memory could burn even with a few entries. (The snapshot doesn't report high memory usage for those entries, though! The image and canvas data is stored natively.) So keep that in mind when handling those.

No comments:

Post a Comment