function foo(arg) { bar = "this is a hidden global variable"; }
<avm@pendragon.be>Most frequent causes:
Even without leak, the memory may be the origin of performance issues:
window object
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"; }
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"; }
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);
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);
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.
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.
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>
elements → tableCell → 'td-3-7' <td> element → …
(since each table element holds a reference to the table itself!)
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).
→ heap size over time
⇒ find out if memory is regularly increasing
in Chrome:
>> Developer Tools >> Performance >> ⊠ Memory >> ⚫ (Record)
→ capture whole heap
in Chrome:
>> Developer Tools >> Memory >> Profiles >> ⊙ Take Heap Snapshot >> Take Snapshot
Comparison:
Detached nodes:
→ 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.
→ breakdown of allocations by JavaScript function
in Chrome:
>> Developer Tools >> Memory >> Profiles >> ⊙ Record Allocation Profile >> Start
Then, select "Heavy (Bottom Up)" instead of "Chart"
→ detect leak remaining after navigating away from a page
Take 3 snapshots as follows:
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.
<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>
<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>
<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>