Cyclic adding/removing of DOM nodes causes memory leaks in JavaScript? - javascript

I'm trying to display dynamically changeable data manipulating with DOM elements (adding/removing them). I found out a very strange behavior of almost all browsers: after I removed a DOM element and then add a new one the browser is not freeing the memory taken by the removed DOM item. See the code below to understand what I mean. After we run this page it'll eat step-by-step up to 150 MB of memory. Can anyone explain me this strange behavior? Or maybe I'm doing something wrong?
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<script type="text/javascript">
function redrawThings() {
// Removing all the children from the container
var cont = document.getElementById("container");
while ( cont.childNodes.length >= 1 ) {
cont.removeChild(cont.firstChild);
}
// adding 1000 new children to the container
for (var i = 0; i < 1000; i++) {
var newDiv = document.createElement('div');
newDiv.innerHTML = "Preved medved " + i;
cont.appendChild(newDiv);
}
}
</script>
<style type="text/css">
#container {
border: 1px solid blue;
}
</style>
</head>
<body onload='setInterval("redrawThings()", 200);'>
<div id="container"> </div>
</body>
</html>

I can't reproduce this on FF 3.6.8/Linux, but 200 ms for a timer is rather small with that much of DOM re-rendering. What I notice on my machine is that when doing JavaScript-intensive things besides running this script, like typing in this answer box, memory usage increases, but is released again when I stop typing (in my case, to something around 16% of memory usage).
I guess that in your case the browser's garbage collector just doesn't have enough ‘free time’ to actually remove those nodes from memory.

Not sure if it'll affect timing, and it's probably really bad practice, but instead of looping through the child nodes, could you not just set the innerHTML of the div to "" ??

It's because removing a Node from the DOM Tree doesn't delete it from memory, it's still accessible, so the following code will work:
var removed = element.removeChild(element.firstChild);
document.body.appendChild(removed);
That code will remove the first child from element, and then after it has been removed, append it to the end of the document.
There really is nothing you can do except make your code more efficient with less removals.
For more info, check out the Node.removeChild page at the Mozilla Developer Center.

Related

Losing a node reference? (javascript)

So I have this JS code :
var pElt = document.createElement("p");
var aElt = document.createElement("a");
aElt.textContent = "Text";
aElt.href = "#";
pElt.appendChild(aElt);
aElt.style.color = "red";
pElt.innerHTML += "<span> and more text</span>";
//aElt.style.color = "red";
document.getElementById("content").appendChild(pElt);
console.log(aElt); // always show the red attribute
There's probably some answer around here, but I cannot even describe the problem ; so I went with "losing node reference", even though it's not what happens here. (edit: in fact, that's what happens here, silly :))
So... Please try the code as it is. It works, the link is red, everyone is happy. Now comment the "aElt.style.color = "red";" line, then uncomment the other one, two lines below.
...
It does not work, the link still appear in black. What I thought is that the pointer linked to my node was either not valid anymore or the aElt was moved to a different memory address. But when I type "console.log(aElt)", it outputs the node correctly (well... I think it does), so I don't get why I can't access it after the .innerHTML change.
What interests me is what happens under the hood :)
Thanks!
index.html :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Question!</title>
</head>
<body>
<div id="content"></div>
<script src="script.js"></script>
</body>
</html>
When you overwrite the content of the <p> element by setting it's innerHTML, you're effectively turning the <a> back into HTML text, appending the <span> (as text), and then recreating new DOM nodes in the <p>. Your old reference still refers to the original <a> you created.
You could instead create that <span> the same way you created the <a>, and append that node to the <p> instead of overwriting .innerHTML.

Element custom properties not being kept after loop

I have some more complex code with a strange behaviour that I've managed to reproduce here:
<!DOCTYPE html>
<html>
<body>
<div>things</div>
<div>stuff</div>
<div>other</div>
<div>misc</div>
<script>
var forEach = function (array, callback, scope) {
for (var i = 0; i < array.length; i++) {
callback.call(scope, array[i], i);
}
}
var d = document.querySelectorAll('div');
d[1].o = d[1].textContent;
forEach(d, function (el, i) {
d[1].innerHTML += '<p>div things</p> sdf d';
document.body.innerHTML += '<div>new div</div> fdsffsd fsdf';
alert(d[1].o);
});
</script>
</body>
</html>
I should get 4 alerts, each saying "stuff". And I do, until I do a hard refresh, and then a normal refresh. Then only the first alert says "stuff", and the others say "undefined". It appears the "o" property being added to div[1] is not being kept. It seems to be related to the innerHTML being added to the body in the loop. The innerHTML being added to the div doesn't seem problematic.
I cannot see what the problem is. Moreover, this only seems to happen in Chrome (v43) and not in Firefox.
Any ideas?
The reason this is happening when the body's innerHTML is updated is that the whole of the body's innerHTML needs to be reparsed. This means any custom properties attached to any elements are then lost, as these DOM elements are being recreated.
Thus one should probably not be using innerHTML with the += operator unless you're sure you know what you're doing.
Why it even worked sometimes is a mystery...

After cloning an element find the original element in the document

I clone my mainSection like this (I have to clone it because, there are new elements added to #main over AJAX, and I don't want to search through them):
$mainSection = $('#main').clone(true);
then i search through the cloned main section for an element:
var searchTermHtml = 'test';
$foundElement = $mainSection.filter(":contains('"+searchTermHtml+"')");
When I find the string 'test' in the #mainSection I want to get the original element from it in the $mainSection so I can scroll to it via:
var stop = $foundElementOriginal.offset().top;
window.scrollTo(0, stop);
The question is: how do I get the $foundElementOriginal?
Since you're changing the content of #main after cloning it, using structural things (where child elements are within their parents and such) won't be reliable.
You'll need to put markers of some kind on the elements in #main before cloning it, so you can use those markers later to relate the cloned elements you've found back to the original elements in #main. You could mark all elements by adding a data-* attribute to them, but with greater knowledge of the actual problem domain, I expect you can avoid being quite that profligate.
Here's a complete example: Live Copy
<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<meta charset="utf-8">
<title>Example</title>
</head>
<body>
<div id="main">
<p>This is the main section</p>
<p>It has three paragraphs in it</p>
<p>We'll find the one with the number word in the previous paragraph after cloning and highlight that paragraph.</p>
</div>
<script>
(function() {
"use strict";
// Mark all elements within `#main` -- again, this may be
// overkill, better knowledge of the problem domain should
// let you narrow this down
$("#main *").each(function(index) {
this.setAttribute("data-original-position", String(index));
});
// Clone it -- be sure not to append this to the DOM
// anywhere, since it still has the `id` on it (and
// `id` values have to be unique within the DOM)
var $mainSection = $("#main").clone(true);
// Now add something to the real main
$("#main").prepend("<p>I'm a new first paragraph, I also have the word 'three' but I won't be found</p>");
// Find the paragraph with "three" in it, get its original
// position
var originalPos = $mainSection.find("*:contains(three)").attr("data-original-position");
// Now highlight it in the real #main
$("#main *[data-original-position=" + originalPos + "]").css("background-color", "yellow");
})();
</script>
</body>
</html>

JavaScript: Unable to read all elements from innerHTML content

I am trying to read the elements in an innerHTML of a div, but it seems that alternate elements are being read.
Code Block:
<!DOCTYPE html>
<html>
<body>
<script type="text/javascript">
var tdiv=document.createElement("div");
tdiv.innerHTML="<span>a1</span><span>a2</span><span>a3</span><span>a4</span><span>a5</span>";
var cn=tdiv.getElementsByTagName("*");
var len=cn.length;
console.log("length: "+len);
console.log("tdiv len: "+tdiv.getElementsByTagName("*").length);
for(var i=0;i<len;i++){
if(cn[i]){
console.log(i+": "+cn[i].nodeName+": "+cn[i].tagName);
document.body.appendChild(cn[i]);
}
}
</script>
</body>
</html>
Output:
a1a3a5
Note: a2 and a4 are missing.
I have tried using both childNodes and getElementsByTagName("*") in all the browsers, IE, FF, Chrome, Opera, Safari and I see the same behavior.
When I add a white space between all the spans then all the elements are being read. Is this an expected behavior ? If so, why ?
The returned item is a live NodeList. You are appending them to the body element, so the NodeList is shrinking with each iteration of the for loop. This is what causes it to appear like it's arbitrarily skipping elements.
Try...
while (cn.length) {
cn[0] && document.body.appendChild(cn[0]);
}
jsFiddle.
When I add a white space between all the spans then all the elements are being read. Is this an expected behavior ? If so, why ?
Yes, it's expected. It just means instead of skipping the span elements, it's skipping the text nodes introduced by the spaces. Never rely on this - it's terribly fragile.

IE7/8 + <time> tag + jQuery .clone() =?

I'll preface this by saying that I already solved this issue by fundamentally changing my approach. But in the process of solving it, I put together a test case that fascinates and vexes me.
I have a string returned from an AJAX call. The string contains HTML, most of which is useless. I want one element from the string (and all its children) inserted into the DOM. A simulation of this is:
<!DOCTYPE html>
<html>
<head>
<title>wtf?</title>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
$(document).ready(function() {
var markup = '<div class="junk"><div class="good"><time datetime="2013-03-29">March 29, 2013</time></div></div>',
output = $(markup).find('.good').clone().wrap('<div />').parent().html();
$('body').append(output);
});
</script>
</head>
<body></body>
</html>
I have a hosted copy of this file up here: http://alala.smitelli.com/temp/wtf_ie.html (won't be up forever).
What this should do is extract the .good div and the child <time> element, then insert them into the body. I do .wrap().parent() to extract the element I selected in addition to its children (see this question). The .clone() and .html() are contrivances that demonstrate the problem.
To the user, it should show today's date. And it works in Chrome, Firefox, IE9, etc.:
March 29, 2013
But in IE7 and 8, the displayed text is:
<:time datetime="2013-03-29">March 29, 2013
The opening < is shown, and a colon has somehow been inserted. The closing </time> tag looks unaffected, and is not shown escaped.
What's going on here? Is this some sort of bug, or an expected behavior? Where is the colon coming from?
EDIT: As far as suggestions to add document.createElement('time') or html5shiv, neither of those seemed to change the behavior.
Very much to my surprise, I find that if I remove jQuery from the equation in terms of actually parsing the markup, the problem goes away (on both IE7 and IE8), even without createElement('time') or a shim/shiv:
<!DOCTYPE html>
<html>
<head>
<title>wtf?</title>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
$(document).ready(function() {
var div, markup, output;
markup = '<div class="junk"><div class="good"><time datetime="2013-03-29">March 29, 2013</time></div></div>';
div = document.createElement('div');
div.innerHTML = markup;
output = $(div).find('.good').clone().wrap('<div />').parent().html();
$('body').append(output);
});
</script>
</head>
<body></body>
</html>
Live Copy | Source
The change there is that I just use the browser's own handling of innerHTML and a disconnected div to parse markup, rather than letting jQuery do it for me.
So I'd have to say this may be a problem with jQuery's handling of HTML fragments on older browsers without support for HTML5 elements. But that would be a significant claim, and significant claims require significant evidence...
But if I change these lines:
div = document.createElement('div');
div.innerHTML = markup;
output = $(div).find('.good').clone().wrap('<div />').parent().html();
to:
div = $(markup);
output = div.find('.good').clone().wrap('<div />').parent().html();
I get the problem (on both IE7 and IE8): Live Copy | Source
So it does start seeming like a jQuery issue...

Categories

Resources