Did You Know that jQuery Leaks Memory like a Fountain? — and there’s a solution for it.

May 7th, 2010 14 Comments »

jquery is a very mature JavaScript library that is fast and concise. So you’d assume that it should have put a nail onto the JavaScript memory leakage coffin. And you will be wrong in your assumption ;)

Actually the problem of JavaScript memory leakage is not new, it has been around for more than 5 years.

There are several reasons of memory leak:

  • 99% of the time it’s closures between DOM world, and JS world.

  • and the rest of the time its XMLHttpRequest object’s memory leakage due to a similar (but nastier) circular dependency in its onreadystatechange event handler.

The latter is nastier, because setting onreadystatechange closure of the ajax object to null does not always release the circular reference.

And jquery 1.4.2 leaks like a fountain in Internet Explorer 7 and above.

Don’t you believe me? Then give this tiny little code snippet a try:

<html>
<head>
<script src="jquery.js"></script>
<script>
	var counter = 0;
	var pipe = null;
	function nill(){}

	function openPipe(){
		if(counter>10000){ return; }
		document.getElementById('TestDiv').innerHTML = counter++;

		/* try to remove memory leak by relasing the circular reference. */
		if (pipe !== null) {
			pipe.onreadystatechange = nill;
			pipe.abort();
			pipe = null;
		}

		createPipe();		
	}
	
	function createPipe(){
		pipe = $.ajax({ url: '/index.php', 
				cache: false, 
				type: 'POST',
				success: onSuccess,
				error: onError
			});
	}
	
	function onSuccess(data) {
		/* try to remove memory leak by relasing the circular reference. */				
		if (pipe!== null) {
			pipe.onreadystatechange = nill;
			pipe.abort();
			pipe = null;
		}
		setTimeout(openPipe,1);
	}	
	
	function onError(xhr, textStatus, errorThrown) {}

	window.onload = openPipe;
</script>
</head>
<body>
	<div id="TestDiv"></div>
</body>
</html>

When executing the above snippet, the memory utilization in IE7(Windows) will be something like this:

Each horizontal line denotes 100MB of memory. That is, jquery has leaked around 200MB of memory in 10,000 ajax calls. The steep decline at the end of the graph is due to closing the browser window and hence freeing up the leaked memory.

If you are building a single-page ajax application, that will heavily use ajax calls, and will stay open for several hours that’s a lot of memory leak, which will make Internet Explorer throw an out of memory error and crash eventually.

So how can you be sure that the leak is due to jQuery’s ajax implementation?

By creating a similar test snippet by using native ajax calls.

<html>
<head>
<script>
var xhr = new XMLHttpRequest();
var url = 'index.php';
var counter = 0;

function openXHR(){
	xhr.open('POST',url, false);
	xhr.setRequestHeader('X-Requested-With','XMLHttpRequest');
	xhr.setRequestHeader('Accept', 'text/javascript, text/html, application/xml, text/xml, */*');
	xhr.onreadystatechange = readyStateChanged;
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	xhr.send('');		
}

function nill(){}
function readyStateChanged(){
	if(xhr.readyState === 4){
			if(counter>10000){ return; }
			var theDiv = document.getElementById('TestDiv');			
			if(theDiv){ theDiv.innerHTML = (counter++);	}
			xhr.onreadystatechange = nill;
			xhr.abort();
			setTimeout(openXHR,1);
	}
}

window.onload = openXHR;
</script>
</head>
<body>
	<div id="TestDiv"></div>
</body>
</html>

And here’s the corresponding memory utilization graph for 10,000 consecutive runs:

Smooth as butter on bread ;)

Calling native XmlHttpRequests instead of jQuery’s $.ajax got rid of the leak for good.

But wait a second, doesn’t jQuery use browser’s native XmlHttpRequest object when available as well? Actuall it does:

Here’s the related code part from jquery1.4.2 (between lines 4948 and 4960)

// Create the request object; Microsoft failed to properly
// implement the XMLHttpRequest in IE7 (can't request local files),
// so we use the ActiveXObject when it is available
// This function can be overriden by calling jQuery.ajaxSetup
xhr: window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject) ?
function () {
	return new window.XMLHttpRequest();
} :
function () {
	try {
		return new window.ActiveXObject("Microsoft.XMLHTTP");
	} catch (e) { }
},

As the above code implies, even so jQuery does inherently use native XmlHttpRequest, it fails to prevent the leak. That may (most probably) be due to a circulary depencency or closure hidden inside jQuery core.

This leaking situation demonstrates itself only in Microsoft Internet Explorer (version 7 and above) as far as my tests are concerned. And, oddly enough, using the ActiveX counterpart, instead of the brand new native XmlHttpRequest object prevents the leak.

So if we modify the above lines as follows, jQuery will not leak at all:

// Create the request object; Microsoft failed to properly
// implement the XMLHttpRequest in IE7 (can't request local files),
// so we use the ActiveXObject when it is available
// This function can be overriden by calling jQuery.ajaxSetup
xhr: window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject) ?
function () {
	/* v.o.: begin memory leak fix. */
	if (window.ActiveXObject) {
		try {
			return new window.ActiveXObject("Microsoft.XMLHTTP");
		} catch (e) { }
	}
	/* v.o.: end memory leak fix. */
	return new window.XMLHttpRequest();
} :

The above hack simply tries to use an ActiveX object for the AJAX request if there’s one available, and fall back to XmlHttpRequest object if we fail to initialize the ActiveX object.

You can download a (90KB zipped) sample test bundle of the above codes, open task manager, run the codes, test and experience the leak for yourself ;)

It’s surprising and intriguing that the world’s most widely adopted JavaScript library’s implementation has overlooked this memory leakage issue.

If you’ve been experiencing a similar problem, I hope that this article has helped you save some development time that you can spare on more useful things than debugging the jQuery core…

…like writing some actual jQuery code ;)