Content

Memory leaks

Most frequent causes:

Memory hindrance

Even without leak, the memory may be the origin of performance issues:

Leak cause: Global variables

Leak cause: Global variables

Default scope

function foo(arg) {
    bar = "this is a hidden global variable";
}

is equivalent to:

function foo(arg) {
    window.bar = "this is a hidden global variable";
}

Leak cause: Global variables

Wrong usage of this

function foo(arg) {
    this.bar = "this is a hidden global variable";
}

is also equivalent to:

function foo(arg) {
    window.bar = "this is a hidden global variable";
}

Leak cause: Global variables

Tricky usage of this

Given:

var car = {
  brand: "Nissan",
  setBrand: function(_brand) { this.brand = _brand; }
};

var setCarBrand = car.setBrand;
setCarBrand("Toyota");

is equivalent to:

window.brand = "Toyota"

Solution:

var setCarBrand = car.setBrand.bind(car);

Leak cause: Timers or Callbacks

Callbacks and cyclic references

Let’s consider:

var element = document.getElementById('button');
function onClick(event) { element.innerHtml = 'text'; }
element.addEventListener('click', onClick);
// Do stuff

We have a loop of references:
element'click' event → onClick function → element

Some (old) browsers do not collect those cyclic references well.

Solution: remove the callback before disposing the element:

// Now we are about to remove element
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

Leak cause: Timers or Callbacks

Timers

Let’s consider:

var someResource = ...
setInterval(function() {
  var element = document.getElementById('button');
  if (element) {
    // Do stuff with element and someResource, eg:
    element.innerHTML = someResource.toHtml();
}, 1000)

The references graph is like this:
interval → setInterval handler function → element and someResource

Even if the button (referenced through element) is removed, the handler (setInterval) won’t be collected, since the interval is still active. Hence, someResource cannot be collected.

Solution: disable the interval when removing the button.

Leak cause: Detached nodes

For example:

var elements = {
    buttonElt: document.getElementById('button'),
    imageElt: document.getElementById('image')
};

function doStuff() {
    elements.imageElt.src = 'http://some.url/image';
    elements.buttonElt.click();
}

References graph:
- DOM → 'button' and 'image' elements
- doStuff function → elements
. . → buttonElt'button' element
. . → imageElt'image' element

Even if we remove the button:

document.body.removeChild(document.getElementById('button'));

it won’t be collected, because there is still a reference chain through the buttonElt reference stored in elements.

Leak cause: Detached nodes

Even worst:

var elements = {
    tableCell: document.getElementById('td-3-7')
    // where 'td-3-7' is the id of a <td> element inside a big <table>
};
document.body.removeChild(document.getElementById('tab'));
// where 'tab' is the id of this big <table>

Even though only one small reference is held by elements, the whole table will stay stuck in memory.

Indeed, before the remove, the reference graph is as follows:
DOM → 'tab' <table> → … → 'td-3-7' <td> element → 'tab' <table>
elementstableCell'td-3-7' <td> element → …

(since each table element holds a reference to the table itself!)

Leak cause: Closures

A closure is an anonymous function that capture variables from parent scopes.

Because of this property, they can be the cause of memory leaks in very specific cases (see exercise).

Detection techniques

Technique: Timeline recording

→ heap size over time

⇒ find out if memory is regularly increasing

in Chrome:
>> Developer Tools >> Performance >> ⊠ Memory >> ⚫ (Record)

Technique: Timeline recording

images/example-timeline.png

Technique: Heap snapshot

→ capture whole heap

in Chrome:
>> Developer Tools >> Memory >> Profiles >> ⊙ Take Heap Snapshot >> Take Snapshot

Comparison:

Detached nodes:

Technique: Heap snapshot

images/example-snapshots-1.png

Technique: Heap snapshot

images/expanded-detached.png

Technique: Allocation timeline

→ allocation over time

in Chrome:
>> Developer Tools >> Memory >> Profiles >> ⊙ Record Allocation Timeline >> Start

Then select a time interval, to see which allocations occured during the time span.

Technique: Allocation timeline

images/example-recordedallocs-overview.png

Technique: Allocation profiler

→ breakdown of allocations by JavaScript function

in Chrome:
>> Developer Tools >> Memory >> Profiles >> ⊙ Record Allocation Profile >> Start

Then, select "Heavy (Bottom Up)" instead of "Chart"

Technique: Allocation profiler

images/allocation-profile.png

Technique of the 3 snapshots

→ detect leak remaining after navigating away from a page

Take 3 snapshots as follows:

Technique of the 3 snapshots

Analyse the data:

→ In the snapshot 3 (i.e. only objects existing on the second page A), we are filtering by objects that were allocated between the first page A and the page B.

In other words, we see only objects from the first page A that survived up to the second page A.

Technique of the 3 snapshots

images/3_snapshots_technique.png
Figure 1. 3 snapshots technique in Chrome 58.0.3029.110 (64-bit)

Technique of the 3 snapshots

images/SnapshotComparison.png

Exercise: Wrong scope

wrongScope.html
<script>
function randChar() {
  return Math.round(Math.random()*36).toString(36)
}
function mkRandString(size) {
  str = new Array(2<<size);
  for (i=0; i<str.length; str[i++] = randChar());
  return str.join('');
}
var data;

data = mkRandString(20);

// Do something...

data = null;
</script>

Exercise: Detached nodes

detachedDOM.html
<button type="button" id="create">Create</button>
<script>
var reg = [];
function mkString(size, fill) {
  return new Array(2<<size).join(fill);
}
function small() { return mkString(8, 'X'); }
function big() { return mkString(20, 'X'); }

function create() {
  var ul = document.createElement('ul');
  for (var i = 10; i-->0;) {
    var li = document.createElement('li');
    li.data = big();
    li.more = small();
    ul.appendChild(li);
  }
  detachedTree = ul;
  reg.push(detachedTree);
}

document.getElementById('create').
    addEventListener('click', create);
</script>

Exercise: Closure leak

closureLeak.html
<script>
var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(2<<20).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 5000);
</script>

Exercise: More…

References

That’s all folks!