In JavaScript, we rarely think about memory management. It appears natural that we create variables, use them and the browser takes care about low-level details.
But as applications become complex and AJAXy, and a visitor stays on a page for a long time, we may notice problems like a browser takes 1G+ and grows larger and larger in size. That’s usually because ofmemory leaks.
Here we discuss the memory management and most frequent types of leaks.
Memory management in JavaScript
The central concept of JavaScript memory management is a concept of reachability.
- A distinguished set of objects are assumed to be reachable: these are known as the roots. Typically, these include all the objects referenced from anywhere in the call stack (that is, all local variables and parameters in the functions currently being invoked), and any global variables.
- Objects are kept in memory while they are accessible from roots through a reference or a chain of references.
There is a Garbage Collector in the browser which cleans memory occupied by unreachable objects.
Garbage collection example
Let’s create see how it works on the following code:
03 | this .elem = document.getElementById( 'id' ) |
06 | var menu = new Menu( 'My Menu' ) |
08 | document.body.innerHTML = '' |
10 | menu = new Menu( 'His menu' ) |
Here we have the memory structure:
On step (1), body.innerHTML
is cleaned. So, technically, it’s children are removed, because they are not accessible any more.
…But the element #id
is an exception. It is accessible as menu.elem
, so it stays in memory. Of course if you check it’s parentNode
, it would be null
.
Individual DOM elements may remain in memory even when the parent is cleaned out.
On step (2), reference window.menu
is reassigned, so the old menu becomes inaccessible.
It is automatically removed by the browser Garbage Collector.
Now the full menu structure is deleted, including the element. Of course if there were other references to the element from other parts of the code, then it would stay intact.
Circular references collection
Closures often lead to circular references. For example:
3 | var elem = document.getElementById( 'id' ) |
5 | elem.onclick = function () { |
Here, the DOM element references the function directly via onclick
. And the function references elem
through the outer LexicalEnvironment
.
This memory structure appears even if the handler has no code inside. Special methods likeaddEventListener/attachEvent
also create the reference internally.
The handler is usually cleaned up when the elem
dies:
2 | var elem = document.getElementById( 'id' ) |
3 | elem.parentNode.removeChild(elem) |
Calling cleanUp()
removes elem
from DOM. There’s still a reference LexialEnvironment.elem
, but there are no nested functions, so LexialEnvironment
is recycled. After that, elem
becomes inaccessible and cleaned up with it’s handlers.
Memory leaks
Memory leak happens when the browser for some reason doesn’t release memory from objects which are not needed any more.
This may happen because of browser bugs, browser extensions problems and, much more rarely, our mistakes in the code architecture.
IE<8 DOM-JS memory leak
Internet Explorer prior to version 8 was unable to clean circular references between DOM objects and JavaScript.
The problem was even more serious in IE6 prior to SP3 (mid-2007 patch), because memory was not freed even after page unload.
So, setHandler
leaks in IE<8, elem
and the closure is never cleaned up:
2 | var elem = document.getElementById( 'id' ) |
3 | elem.onclick = function () { } |
Instead of DOM element, there could be XMLHttpRequest or any other COM object.
IE leak workaround is to break circular references.
We assign elem = null
, so the handler doesn’t reference DOM element any more. The circular link is broken.
The partiular leak is mostly of historical interest, but a good example of breaking circular links.
You can read more about it in the articles Understanding and Solving Internet Explorer Leak Patternsand Circular Memory Leak Mitigation.
XmlHttpRequest
memory management and leaks
The following code leaks in IE<9:
01 | var xhr = new XMLHttpRequest() |
03 | xhr.open( 'GET' , '/server.url' , true ) |
05 | xhr.onreadystatechange = function () { |
06 | if (xhr.readyState == 4 && xhr.status == 200) { |
Let’s see the memory structure of each run:
The asynchronous XmlHttpRequest
object is tracked by the browser. So there is an internal reference to it.
When the request finishes, the reference is removed, so xhr
becomes inaccessible. IE<9 doesn’t do that.
There is an separate page example for IE.
Fortunately, fixing this is easy. We need to remove xhr
from the closure and access it as this
in the handler:
01 | var xhr = new XMLHttpRequest() |
03 | xhr.open( 'GET' , 'jquery.js' , true ) |
05 | xhr.onreadystatechange = function () { |
06 | if ( this
.readyState == 4 && this
.status == 200) { |
07 | document.getElementById( 'test' ).innerHTML++ |
Now there is no circular reference, so the leak is fixed. Sample page for IE.
setInterval/setTimeout
Functions used in setTimeout/setInterval
are also referenced internally and tracked until complete, then cleaned up.
For setInterval
, the completeness occurs on clearInterval
. That may lead to memory leaks when the function actually does nothing, but the interval is not cleared.
For server-side JS and V8 see an example of that in the issue: Memory leak when running setInterval in a new context.
Memory leak size
Data structures which leak may be not large.
But the closure makes all variables of outer functions persist while the inner function is alive.
So imagine you create an function and one of its variables contains a large string.
02 | var data = "Large piece of data, probably received from server" |
While the function inner
function stays in memory, then the LexicalEnvironment
with a large variable inside will hang in memory until the inner function is alive.
JavaScript interpreter has no idea which variables may be required by the inner function, so it keeps everything. In every outer LexicalEnvironment
. I hope, newer interpreters try to optimize it, but not sure about their success.
Actually, there may be no leak. Many functions may be created for sane reason, for example per every request, and not cleaned up because they are handlers or something.
If the data
is only used in the outer function, we could nullify it to save memory.
02 | var data = "Large piece of data, probably received from server" |
Now the data
still remains in memory as the property of LexicalEnvironment
, but it doesn’t occupy so much space.
jQuery anti-leak measures and leaks
jQuery uses $.data API to fight IE6-7 memory leaks. Unfortunately, that introduces new jQuery-specific leaks.
The core principal of $.data
is that any JavaScript entity is bound to/read from an element using jQuery call:
3 | $(document.body).data( 'prop' , 'val' ) |
4 | alert( $(document.body).data( 'prop' ) ) |
The jQuery $(elem).data(prop, val)
assignment does the following:
- The element gets an unique number if it doesn’t have any:
elem[ jQuery.expando ] = id = ++jQuery.uuid |
Here, jQuery.expando
is a random key, so there will be no conflicts. - The data is set to a special object
jQuery.cache
:
jQuery.cache[id][ 'prop' ] = val |
When the data is read from an element:
- The element unique number is retrieved from
id = elem[ jQuery.expando ]
. - The data is read from
jQuery.cache[id]
.
The purpose of this API is that a DOM element never references JavaScript objects direclty. It has a number, but that’s safe. The data is in jQuery.cache
. Event handlers internally use $.data
API also.
But as a side effect, an element can’t be removed from DOM using native calls.
Leak examples
The following code leaks in all browsers:
2 | .html( new Array(1000).join( 'text' )) |
6 | document.getElementById( 'data' ).innerHTML = '' |
Open on a separate page.
The leak happens because elem
is removed by cleaning parent innerHTML
, but the data remains injQuery.cache
. More importantly, the event handler references elem
, so both handler and elem
stay in memory with the whole closure.
A simpler leak example:
The code below leaks:
3 | .html( new Array(1000).join( 'text' )) |
Open on a separate page.
The problem is: the element is created, but not put anywhere. So, after the function, the reference to it is lost. But it’s jQuery.cache
data persists.
Leak evasion
First, one should use jQuery API to removing elements.
Methods remove(), empty() and html() check descendant elements for data and clean them. That’s the overhead, but, at least, the memory can be reclaimed.
Actually, if performance is critical, there are tweaks.
- First, if you know which elements have handlers (and there are few of them),
you may want to clean the data manually with removeData() and then you’re safe.
Now can use detach() which doesn’t clean data or any native method. - If you don’t like the way above, but the DOM tree is large, you may use
$elem.detach()
and put$(elem).remove()
in setTimeout
, so it will act asynchronously and evade visual sluggishnes.
Fortunately, finding jQuery memory leaks is easy. Check the size of $.cache
. If it’s too large, inspect it and see which entries stay and why.
Now when the topic is (hopefully) clear, in the next sections we are not talking about jQuery leaks.
Finding and fixing memory leaks
Checking for leaks
There are many leak patterns and browser bugs. New browser bugs will appear, because programming is hard.
So, we could meet a leak in HTML5 functionality or any other place. To fix, we should try to isolate and reproduce it first.
The browser doesn’t clean memory immediately. Most algorithms of garbage collection free memory from time to time. The browser may also postpone memory cleanup until the certain limit is occupied.
So it you think you’ve found a problem and run the “leaking” code in a loop, wait for a while.
The browser may grow in size, but eventually free the memory after some time, or when it exceeds the certain value.
Don’t take a minute of increasing memory as a loop proof if the overall footprint is still low. Add something to leaking objects. A large string will do.
Preparing the browser
Leaks may occur because of browser extensions, interacting with the page. More importantly, a leak may occur because of two extensions interaction bugs. It’s like: when Skype extension and my Antivirus are enabled, it leaks. When any of them is off, it doesn’t.
So, the steps:
- Disable Flash.
- Disable antiviruses. Link checkers and other functions, integrated with the browser.
- Disable plugins. All plugins.
In Chrome developer tools, there is a Timeline - Memory tab:
We can watch the memory occupied by it.
There is also Profiles - Memory, where we can take a snapshot and see what’s inside. Snapshots can be compared to each other:
Most of time, it doesn’t tell you anything. But at least you can see which objects are piling up, and probably the structure of the leak.
Memory leaks are hard. One thing you certainly need when fighting them.