Memory leaks with jQuery in IE 7/8/9 and FF 4
Asked Answered
P

2

6

I have been struggling with some memory leaks using jQuery in all major browsers, and am looking for some help. I've read many articles on memory leaks, references, circular references, closures, etc. I think I'm doing everything right. But am still seeing memory increases in IE9 and FF4 and orphans in sIEve that just don't make sense to me.

I've created a test case to showcase the issue. The test case basically has a large table of 500 rows, and users can click on each row to enter an inline edit mode where elements are appended using jQuery. When users exit inline edit mode, the elements are removed.

The test case has a button to emulate 100 clicks for 100 rows to quickly magnify the issue.

When I run it in sIEve, memory increases 1600KB, in use goes up by 506, and there are 99 orphans. Interestingly, if I comment out the .remove() on line 123, memory increase 1030KB, in use goes up by 11, and there are 0 orphans.

When I run it in IE9, memory increase 5900KB. A refresh increases another 1500KB and another run increases anotherr 1K. Continuing this pattern continues the memory use increase

When I run it in FF4, I get very different behavior if I use "100 clicks slow" and "100 clicks fast". Emulating slow clicks has a peak increase of 8300KB and it takes a minute for it to settle down to 3300KB. Emulating fast clicks has a peak increase of 27,700KB, then it takes a minute to settle down to 4700KB. Note this is the exact same code that is executed, just with less delays between executing. A refresh and another run continues to increase memory at a similar rate.

Sample code:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Strict//EN">
<html>
<head>
 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
 <script type="text/javascript">
var clickcounter = 0;
var clickdelay = 0;
var clickstart = 0;
var clicklimit = 0;

$(document).ready(function(){
 // Create a table with 500 rows
 var tmp = ['<table>'];
    for (var i = 0; i < 500; i++) {
  tmp.push('<tr id="product_id',i+1,'" class="w editable">');
  tmp.push('<td class="bin">',i+1,'</td>');
  tmp.push('<td class="productcell" colspan="2">Sample Product Name<div class="desc"></div></td>');
  tmp.push('<td class="percentcost">28%</td>');
  tmp.push('<td class="cost">22.50</td>');
  tmp.push('<td class="quantity">12</td>');
  tmp.push('<td class="status">Active</td>');
  tmp.push('<td class="glass">23</td>');
  tmp.push('<td class="bottle">81</td>');
  tmp.push('</tr>');
 }
 tmp.push('</table>');
 $('body').append(tmp.join(''));

 // Live bind a click event handler to enter Inline Edit
 $('tr.w').live('click', function(event){
  var jrow = $(this);
  if (!jrow.find('.inedBottle').length) {
   createInlineEdit(jrow);
  }
 });

 // This is just to emulate 100 clicks on consecutive rows
 $('#slow100').click(function() {
  clickstart = clickcounter;
  clickcounter++;
  var jrow = $('#product_id'+clickcounter);
  createInlineEdit(jrow);
  clickdelay = 1000;
  clicklimit = 100;
  window.setTimeout(clickemulate, clickdelay);
 });

 // This is just to emulate 100 rapid clicks on consecutive rows
 $('#fast100').click(function() {
  clickstart = clickcounter;
  clickcounter++;
  var jrow = $('#product_id'+clickcounter);
  createInlineEdit(jrow);
  clickdelay = 20;
  clicklimit = 100;
  window.setTimeout(clickemulate, clickdelay);
 });

});

// Emulate clicking on the next row and waiting the delay period to click on the next
function clickemulate() {
 if ((clickcounter - clickstart) % clicklimit == 0) return;
 nextInlineEdit($('#product_id'+ clickcounter));
 clickcounter++;
 window.setTimeout(clickemulate, clickdelay);
}

// Enter inline edit mode for the row
function createInlineEdit(jrow, lastjrow) {
 removeInlineEdit(lastjrow); 

 jrow.removeClass('editable').addClass('editing'); 

// Find each of the cells
 var productcell = jrow.find('.productcell');
 var bincell = jrow.find('.bin');
 var percentcostcell = jrow.find('.percentcost');
 var costcell = jrow.find('.cost');
 var glasscell = jrow.find('.glass');
 var bottlecell = jrow.find('.bottle');
 var descdiv = productcell.find('.desc');

 var product_id = jrow.attr('id').replace(/^product_id/,'');

// Replace with an input
 bincell.html('<input class="inedBin" name="bin'+product_id+'" value="'+bincell.text()+'">');
 costcell.html('<input class="inedCost" name="cost'+product_id+'" value="'+costcell.text()+'">');
 glasscell.html('<input class="inedGlass" name="glass'+product_id+'" value="'+glasscell.text()+'">');
 bottlecell.html('<input class="inedBottle" name="bottle'+product_id+'" value="'+bottlecell.text()+'">');
 var tmp = [];
// For one input, insert a few divs and spans as well as the inputs.
// Note: the div.ined and the spans and input underneath are the ones remaining as orphans in sIEve
 tmp.push('<div class="ined">');
 tmp.push('<span>Inserted Span 1</span>');
 tmp.push('<span>Inserted Span 2</span>');
 tmp.push('<input class="inedVintage" name="vintage',product_id,'" value="">');
 tmp.push('<input class="inedSize" name="size',product_id,'" value="">');
 tmp.push('</div>');
 tmp.push('<div class="descinner">');
 tmp.push('<input class="inedDesc" name="desc'+product_id+'" value="'+descdiv.text()+'">');
 tmp.push('</div>');

 descdiv.html(tmp.join(''));

 jrow.find('.inedVintage').focus().select();
}

// Exit the inline edit mode
function removeInlineEdit(jrow) {
 if (jrow && jrow.length) {
 } else {
  jrow = $('tr.w.editing');
 }

 jrow.removeClass('editing').addClass('editable');

// Note: the div.ined and the spans and input underneath are the ones remaining as orphans in sIEve
// sIEve steps: load page, click "Clear in use", click "100 clicks fast" on the page
// If the remove is commented out, then sIEve does not report any div.ined as orphans and reports 11 in use (div.ined all appear to be garbage collected)
// If the remove is uncommented, then sIEve reports 99 of the div.ined as orphans and reports 506 in use (none of the div.ined garbage collected)

 jrow.find('.ined').remove(); 
 jrow.find('.inedBin').each(function() {
  $(this).replaceWith(this.defaultValue);
 });
 jrow.find('.inedGlass').each(function() {
  $(this).replaceWith(this.defaultValue);
 });
 jrow.find('.inedBottle').each(function() {
  $(this).replaceWith(this.defaultValue);
 });
 jrow.find('.inedCost').each(function() {
  $(this).replaceWith(this.defaultValue);
 });
 jrow.find('.inedDesc').each(function() {
// Since the div.ined is under here, this also removes it.
  $(this).closest('.desc').html(this.defaultValue);  
 });
}

function nextInlineEdit(jrow) {
 var nextjrow = jrow.nextAll('tr.w').first();
 if (nextjrow.length) {
  createInlineEdit(nextjrow, jrow);
 } else {
  removeInlineEdit(jrow);
 }
}

 </script>
 <style>
table {margin-top: 30px;}
td {border: 1px dashed grey;}
button#slow100 {position: fixed; left: 0px; width: 115px;}
button#fast100 {position: fixed; left: 120px; width: 115px;}
 </style>
</head>
<body>
 <button id="slow100">100 clicks slow</button>
 <button id="fast100">100 clicks fast</button>
</body>
</html>
Papaya answered 12/6, 2011 at 17:5 Comment(8)
You do realize most browsers use memory pooling right? I don't know how you're doing your profiling, but if you're just using the "memory used" column in task manager, it's not going to be too accurate. Most browsers will allocate memory which then gets divided up internally. Even if the memory isn't being used it will still be listed as "in use".Scalf
Also the 1KB increase you're seeing may be nothing more than the caching mechanism.Scalf
@Chris: I'm somewhat familiar but far from a guru on how browsers use memory. I'm learning still, which is part of my question. I am using the "Working Set" column in the Resource Monitor in Windows 7, which should be the same as the Task Manager's "Memory (Private Working Set)" column plus the sharable memory. I don't know what would be caching. The page is not being navagated away from when I click on the rows: it's just dynamically adding input elements and removing them. Would IE9 or FF4 cache any of this activity within one web page? That's not my understanding.Papaya
@lschult2: yeah, working set is just the amount reserved for the process. So let's say that your script uses 10MB (just for kicks). It will ask IE for 10MB. IE will check its own memory pool. Let's say for the sake of argument that it doesn't have 10MB available. It might turn around to the OS and ask for, oh say, 50MB. It will then turn around and hand 10MB to your script, and keep the other 40 around for later. Resource manager will show 50MB in use. When your script is done, the 10 will be put back, but IE will still look like it's using 50. It looks like a leak, but isn't.Scalf
And the guess about caching was just a guess. It could be all kinds of things. History, maybe? For all I know you may have discovered a totally legitimate leak.Scalf
@Chris: the problem is that the Working Set just keeps growing and growing. If I use my app for several hours, I've seen IE9 up to 200MB and FF4 up to 800MB. I've traced it all to this as the source. So the problem is it's not putting the 10 back. It just keeps taking more and morePapaya
Just a guess here: if it's happening in more than one browser (with different javascript engines), it's probably not your code. And firefox is known to be a very bad offender when it comes to memory.Scalf
I also had "memory leaks" using jQuery and saw memory usage go up quickly. Then I discovered that waiting a little time (halting script to prevent new memory usage) the memory would eventually go down to normal levels, cause the browser started to clean the unused memory. Using your code by the way I didn't had any memory leaks or any tipe, used memory floating up and down as normal. It's hard to tell what the cause can be.Mylohyoid
L
1

I recent technique I've learnt is to instead of binding events to every object you want clicked, bind one click to the page and look at the event to what object has been clicked.

This is much faster for multiple reasons:

  • There are way less bind rules for your browser to go through (which older FF and IE are poor at handling, you prob don't notice these issues on Opera or Chrome)
  • The whole process is much faster.

Example:

$(body).click( function (event) {
    $target = $(event.target);
    if ( $target.hasClass('w') ) {
        var jrow = $target;
        if (!jrow.find('.inedBottle').length) {
        createInlineEdit(jrow);
    }
});

this is off of the top of my head. There is prob a better way than checking the class but I'm a little under the weather.

I hope it helps!

Lunde answered 7/7, 2011 at 13:22 Comment(0)
C
-1

Maybe 1 problem could be the use of .live. I'm not sure how .live works internally, but it must listen to an event that changes the DOM (every time you update/replace the elements in your td's, and if there is a new <td class="w"> it observes it with the given callback). If the .live method is not ready checking for another $("td.w") included to the DOM and you start another check, it can cause those memory leaks. I'm just guessing, but try to replace this code:

 $('tr.w').live('click', function(event){
   var jrow = $(this);
   if (!jrow.find('.inedBottle').length) {
    createInlineEdit(jrow);
   }
 });

with this:

 $('tr.w').bind('click', function(event){
   var jrow = $(this);
   if (!jrow.find('.inedBottle').length) {
    createInlineEdit(jrow);
   }
 });

and maybe your memory usage is getting lower, during the heavy DOM operations. Let me know if this helps!

Cuneal answered 2/7, 2011 at 11:44 Comment(2)
I actually used .bind before, and found that is more likely to cause memory leaks since a bound event handler can create the circular references. jQuery recommends .live over .bind since .live is supposed to handle that correctly. link linkPapaya
I do not know if that's correct for elements, which are already in the DOM and are not affected by replacements ...Cuneal

© 2022 - 2024 — McMap. All rights reserved.