I've got a loop where I insert about 500 <div>s into the DOM via document.getElementById(target).appendChild(docFragmentDiv), and each <div> has probably 20 or 30 descendants. I can insert up to about 100 just fine, but the page temporarily freezes while the DOM is updated if there's something like 200+ inserts.
How can I avoid this choke? Right now I have an interval set up to insert 50 per interval, with 25 ms between intervals. Is there a better way to do this?
You should use innerHTML, and you should use it only once.
For optimal efficiency, you should assemble pieces of HTML in an array of strings using a loop, then write document.getElementById(target).innerHTML = htmlPieces.join()
Using innerHTML like SLaks suggests will work, but it's probably better practice to create a document fragment, append all of your div nodes to that, then append the document fragment to the main document.
var docFrag = document.createDocumentFragment();
for (var i=0; i<500; i++) {
docFrag.appendChild(document.createElement("div"));
}
document.getElementById(target).appendChild(docFrag);
innerHtml() is almost always faster than appendChild().
Have a look at QuirksMode's benchmarking for real numbers.
Edit: Is there any reason why you're adding them in intervals rather than all at once? I think SLaks is right about doing it all at once.
I'm not sure if this would be any faster, but it would be the first thing I tried:
Instead of adding them all to the page, add them all to a container that you haven't yet added to the page, and then add that container to the page.
If that improves performance, then you can modify it to suit your needs.
Be careful that you aren't using appendChild to the actual document until the last instant. In other words, if you're doing:
var div1 = document.createElement("div");
document.body.appendChild(div1);
for (var i=0; i<500; i++) {
var div = document.createElement("div");
div1.appendChild(div);
}
Then you are redrawing the entire page every time you iterate in that loop.
Better, if you're using DOM methods, to reverse this:
var div1 = document.createElement("div");
for (var i=0; i<500; i++) {
var div = document.createElement("div");
div1.appendChild(div);
}
document.body.appendChild(div1);
Now you are drawing your divs to an offscreen buffer, and only making the page redraw once.
Related
Similar to this question I want to change some text inside a dynamic div. The explanation there didn't work for me so I started a new thread (rep too low to comment). What makes this div "dynamic" is a some script that calculate how much money you need to spend (based on added to cart items) to get free shipping. The statement I want to replace is always there. I guess you could call it erasing the part of the text. :)
My div:
<div class="free-shipping__content">Brakuje Ci 151,00 zł do darmowej dostawy (Paczkomaty InPost).</div>
My code in GTM (loaded on DOM ready):
<script type="text/javascript">
(function(){
var elements = document.querySelectorALL(".free_shipping__content");
for (i = 0; i < elements.length; ++i) {
var str = elements[i].innerHTML;
elements[i].innerHTML = str.replace(" (Paczkomaty InPost)", "");
}
})();
</script>
Thanks!
Image of surrounding divs
Good job on that closure.
document.querySelectorALL is not a function. You meant document.querySelectorAll.
Another problem is your css selector. You're using .free_shipping__content while the actual class name is different. Your selector should be .free-shipping__content
After these fixes, your code works. However, I already rewrote it to test properly, so I might as well offer a more readable and elegant solution:
document.querySelectorAll(".free-shipping__content").forEach(function (el) {
el.innerHTML = el.innerHTML.replace(" (Paczkomaty InPost)", "");
});
Can anyone please explain briefly what documentFragment actually does? I have been searching for a clear explanation but I don't get any until now.
what I read is, documentFragment is something like DOM like structure where we can add modify DOM elements without interrupting the actual flow of the document.
I also read, documentFragment is faster than appending each element into DOM one by one. It felt to me like, documentFragment does not recalculate styles every time so it is faster.
I have two examples,
DOING IT IN FRAGMENT WAY:
var frag = document.createDocumentFragment();
var div1 = document.createElement("div");
var div2 = document.createElement("div");
frag.appendChild(div1);
frag.appendChild(div2);
document.getElementById("someId").appendChild(frag);
DOING IT IN NORMAL WAY:
var div = document.createElement("div");
var div1 = document.createElement("div");
var div2 = document.createElement("div");
div.appendChild(div1);
div.appendChild(div2);
document.getElementById("someId").appendChild(div);
what actually happens in the above two examples?
There's an important difference between "the fragment way" and "the normal way":
Using document.createElement:
const div = document.createElement('div');
div.appendChild(document.createTextNode('Hello'));
div.appendChild(document.createElement('span'));
document.body.appendChild(div);
console.log(div.childNodes); // logs a NodeList [#text 'Hello', <span>]
This results in the following DOM structure:
<body>
<div>
Hello
<span></span>
</div>
</body>
Using DocumentFragment:
const frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode('Hello'));
frag.appendChild(document.createElement('span'));
document.body.appendChild(frag);
console.log(frag.childNodes); // logs an empty NodeList
This results in the following DOM structure:
<body>
Hello
<span></span>
</body>
That means that calling appendChild or insertBefore with an instance of DocumentFragment moves the child nodes of the document fragment to the new parent node. After that, the document fragment is empty.
As you have correctly mentioned, it can be more efficient to create a document fragment and append multiple elements to it than to append those elements to the real DOM one by one, causing the browser to re–render parts of the page every time. Because the contents of the document fragment are not visible on screen, the page has to be re–rendered only once.
Whenever you create a large DOM structure, it can be advisable to create it within a document fragment and append that to the DOM when you're done.
For appending only 2 childs you will not see any performance issue. Imagine that you have an array of books that includes 100 items and you want to append them to the DOM. You would write this code:
let books=[,,,,,,,,,,,,,,,,]
let bookList;
document.addEventListener('DOMContentLoaded',load)
function load(){
bookList=document.getElementById('books')
books.forEach(book=>{
let li=document.createElement('li')
li.textContext=book
li.class="bookItem"
// here is the headache part
bookList.appendChild(li)
}))
}
So you are going to loop through 100 times and each time, you are going to tell the browser that redraw the screen. This will take up a lot of resources. Depending on the system that you are using, you might see the screen is flickering.
with fragment I would write load like this:
function load(){
bookList=document.getElementById('books')
let df=new DocumentFragment()
books.forEach(book=>{
let li=document.createElement('li')
li.textContext=book
li.class="bookItem"
// we are appending to df not to the bookList
df.appendChild(li)
})
// I collected all list elements inside DocumentFragment
// Now I append it to the bookList
bookList.appendChild(df)
}
creating document fragment is like creating a div. But it does not add any Html to the page. You are not going to see in HTML source code:
<DocumentFragment></DocumentFragment>
It is just an empty container that is used to hold other parts of Html. a fragment can be injected and cloned in a single operation instead of having to inject and clone each
individual node over and over again. We are achieving exact same thing with much better performance.
I've got a for loop in which I am appending rows to a table. Although I append each tr one at a time in the loop, the browser does not render any of them until the loop is finished.
At first, I thought it might be the browser rendering too quickly for me to notice, but after increasing the number of rows to, say, 10000, there's a significant slowdown, then the entire table shows at once.
Link: http://jsfiddle.net/xyan/ad2tV/
Increment counter to increase the number of rows.
Also, if you change counter to 3 (or another small number) and uncomment the alert(), it will pause the loop and show each row being added one at a time.
HTML:
<div></div>
Javascript:
var table = $('<table />').appendTo($('div'));
var counter = 1000;
var html = [];
var j = 0;
for (var i = 1 ; i < (counter + 1) ; i++) {
html[j++] = '<tr>';
html[j++] = '<td>'+i+'-1</td><td>'+i+'-2</td><td>'+i+'-3</td>';
html[j++] = '<tr>';
table.append(html.join(''));
//alert('pause');
html = [];
j = 0;
}
CSS:
table td {
padding: 3px;
border: 1px solid red;
}
Note:
I've found a way to force the loop to slow down, allowing the rows to be added one at a time.
Link: http://jsfiddle.net/xyan/8SCP9/
var html = '';
var numbertorun = 15;
var delay = 500;
(function loop (i) {
setTimeout(function () {
html = '<tr><td>'+i+'-1</td><td>'+i+'-2</td></tr>';
$('table').append(html);
if (--i) loop(i);
}, delay)
})(numbertorun);
So I guess my question isn't how to do this, but why the rows aren't inserted one at a time in the original for loop.
I'm guessing some background is needed to understand this question.
Each time you add a new row to the table in your webpage, your script will alter the model of your document in your browsers memory. The API you are using for this is called DOM (Document Object Model).
An individual update to DOM doesn't always necessarily mean that a page will be redrawn. It is up to the browser to decide, when a given set of DOM alterations will cause the browser to re-render the page.
I don't know for fact, but I'm guessing that browsers typically update views perhaps somewhere around 40-60 times per second. The thing is this: if your table printing takes less time than 1/60th of a second, you don't get to see the partial updates.
By adding a timer you of course can slow down the progress of creating the table. As you perceived, this will allow you to see the page redrawn at different points during the for loop.
If you wish to force browser to redraw the UI, there are some JavaScript hacks you could try applying. One such hack for instance is documented in Ajaxian: Forcing a UI redraw from JavaScript.
If you deliberatively wish to run your program slowly and add rows slowly, say one row per second, while potentially doing some other stuff with JavaScript at the same time, you need to run the loop either by triggering new iterations somehow or by using another (JavaScript) thread.
A schematic example of using setTimeout to slow down printing (prints out 1 row per sec, just tweak second parameter in window.setTimeout to change delay):
var table = $('<table id="table1"/>').appendTo($('div'));
var i = 0;
var counter = 1000;
function addRow() {
table.append('<tr><td>'+i+'-1</td><td>'+i+'-2</td><td>'+i+'-3</td></tr>');
i++;
if(i<counter) {
window.setTimeout( addRow, 1000 );
}
}
addRow();
jQuery append method is similar to javascript appendChild method. Both these methods traverse through the DOM and insert the element at the end of the parent node. This task takes time and doing this inside a Loop will cause frequent changes in DOM Structure which the DOM will not able to replicate. So by inserting your rows using settimeout gives the browser ample time to re-load the content. Hence you see the one by one insertion of rows in the second approach.
Each time an element is added to the page, the browser needs to redraw the entire page. In rendering terms, this is an extremely intensive process and is quite slow. So if you make small changes little by little that will make the page appear really slowly. What the browser is doing is to group the little changes into big changes. The end result is that the page will render much more quickly in aggregate.
The browser is doing this:
http://www.tvidesign.co.uk/blog/improve-your-jquery-25-excellent-tips.aspx#tip6
I have some HTML I need to parse.
Basically I'm walking through the dom of a given element. Grabbing text nodes, and element nodes.
As I come across text nodes, I print them into a different element character by character. Each character is placed into it's own span, with its own style, which was taken from any element node found with a style attached.
So when an element node is found, it's style is applied to any text node detected until another element node is found and the old style is replaced with the new one.
The code below works. If you have a sentence or a short paragraph in the source element it reproduces the text accurately in less than a second. The longer the text gets the longer it takes (duh).
Interestingly, the more text that is already in the destination element, the longer it takes. So if I've ran this function 10 times on the same source element, with the same body of text being processed, it will run slower the 10th time through than the 1st time through, presumably because it's harder to render the text in an element that already has content.
Anyway, I really need to find a way to make this thing run faster.
Lastly, here is an example HTML snippet this thing might need to process:
<span style='blah: blah;'> Some text </span><span>Even more text </span> <p> stuff </p>
The resulting HTML would be:
<span style='blah: blah;'>S</span>
<span style='blah: blah;'>o</span>
<span style='blah: blah;'>m</span>
<span style='blah: blah;'>e</span>
<span style='blah: blah;'> </span>
<span style='blah: blah;'>t</span>
<span style='blah: blah;'>e</span>
<span style='blah: blah;'>x</span>
<span style='blah: blah;'>t</span>
.......
Nothing fancy.
Here's the code:
Code:
ed.rta_to_arr_paste = function(ele, cur_style) {
var child_arr = ele.childNodes;
if(!(is_set(cur_style))) {
cur_style = {};
}
for(var i = 0; i < child_arr.length; i++) {
if(child_arr[i].nodeType == 1) {
if(cur_style != child_arr[i].style) {
cur_style = child_arr[i].style;
}
} else if(child_arr[i].nodeType == 3) {
for(var n = 0; n < child_arr[i].nodeValue.length; n++) {
var span = ed.add_single_char(child_arr[i].nodeValue.charAt(n), cur_style);
}
}
ed.rta_to_arr_paste(child_arr[i], cur_style);
}
}
EDIT:
One example of a system like this being used is in google docs.
When a user pastes text into the document, it's first rendered off screen, then processed with a function similar (I'm assuming) to this one. It then reprints the text in the document.
It all happens extremely fast (unless the text is very long).
It seems you are directly inserting the new elements into the DOM tree, so I think you can get the best improvement by not doing that.
Avoid inserting a lot of elements one by one. Every time you insert an element, the browser has to recalculate the layout of the page and this takes time.
Instead, add the nodes to an element not in the DOM, the best would be using a DocumentFragment, which can be created via document.createDocumentFragment.
Then all you have to do is to insert this fragment and the browser only has to do one recalculation.
Update:
What you could also try is to use regular expressions to convert the text into the span elements.
var html = value.replace(/(.)/g, "<span>$1</span>")
At least in my naive test (not sure if the testcases are good this way), it performs much better than creating span elements and adding them to the document fragment:
Update 2: I adjusted the tests to also set the generated elements/string as content of an element and sadly, this takes away all the speed of using replace. But it might still be worth testing it:
http://jsperf.com/regex-vs-loop
You should also avoid repeated property access:
ed.rta_to_arr_paste = function(ele, cur_style) {
var child_arr = ele.childNodes;
if(!(is_set(cur_style))) {
cur_style = {};
}
for(var i = 0, l = child_arr; i <l; i++) {
var child = child_arr[i];
if(child.nodeType == 1) {
// this will always be true, because `el.style` returns an object
// so comparing it does not make sense. Maybe just override it always
if(cur_style != child.style) {
cur_style = child.style;
}
// doesn't need to be called for other nodes
ed.rta_to_arr_paste(child, cur_style);
}
else if(child.nodeType == 3) {
var value = child.nodeValue;
for(var n = 0, ln = value.length; n < ln; n++) {
ed.add_single_char(value.charAt(n), cur_style);
}
}
}
}
It looks like you are searching the DOM for an element on every call. I would think you would instead attach an event to the DOM elements in something like onload (or better yet use jquery document.ready). I would also (as a minor refactor) first check to make sure you have children ( child_arr.length > 0) prior to call the for loop (this may be totally insignificant but best practice)
I have a page with two frames, and I need to (via javascript) copy an element and all of its nested elements (it's a ul/li tree) and most importantly it's style from one frame to the other.
I get all the content via assigning innerhtml, and I am able to position the new element in the second frame with dest.style.left and dest.style.top and it works.
But I'm trying to get all the style information and nothing's happening.
I'm using getComputedStyle to get the final style for each source element as I loop through each node then and assigning them to the same position in the destination nodelist and nothing happens to visually change the style.
What am I missing?
With getComputedStyle, you can get the cssText property which will fetch the entire computed style in a CSS string:
var completeStyle = window.getComputedStyle(element1, null).cssText;
element2.style.cssText = completeStyle;
Unfortunately, getComputedStyle isn't supported by Internet Explorer, which uses currentStyle instead. Doubly unfortunate is the fact that currentStyle returns null for cssText, so the same method cannot be applied to IE. I'll try and figure something out for you, if nobody beats me to it :-)
I thought about it and you could emulate the above in IE using a for...in statement:
var completeStyle = "";
if ("getComputedStyle" in window)
completeStyle = window.getComputedStyle(element1, null).cssText;
else
{
var elStyle = element1.currentStyle;
for (var k in elStyle) { completeStyle += k + ":" + elStyle[k] + ";"; }
}
element2.style.cssText = completeStyle;
Have you tried cloneNode? It can copy the element and all of its children in one fell swoop.
http://www.w3schools.com/dom/met_element_clonenode.asp
Well I gave up the original tack of trying to get the [computed] style information out of the source tag, I just never got it to work.
So instead I tried this, and it did work...
First I grabbed the -style- tag from the source frame, then I appendChilded it to the end of the -head- tag of the second frame.
For which this proved useful...
How do you copy an inline style element in IE?
Then I made a few nested div elements, each having an id or style class I needed to inherit so that the hierarchy matched the first frame.
Then I shoved the innerhtml of the source frame's tag that I wanted to copy and then finally appendChilded it to the body of the second frame, and magically, it all worked.
var topd = doc.createElement('div'); // make a div that we can attach all our styles to so they'll be inherited
topd.id = 'menuanchorelement';
// shove our desired tag in the middle of a few nested elements that have all the classes we need to inherit...
topd.innerHTML = "<div id='multi-level'><ul class='menu'>" + dh.innerHTML + "</ul></div>";
doc.body.appendChild(topd); // voila