67

Unfortunately I need to iterate over all the DOM elements of a page and I'm wondering what the most efficient technique is. I could probably benchmark these myself and might if I have the time but I'm hoping someone has already experienced this or has some options I hadn't considered.

Currently I'm using jQuery and doing this:

$('body *').each(function(){                                                                                                                            
    var $this = $(this);                                                                                                                                
    // do stuff                                                                                                                                         
});

While it works, It seems to cause some lag on the client. It could also be tweaked with a more specific jQuery context like $('body', '*'). It occurred to me that native Javascript is usually faster than jQuery and I found this:

var items = document.getElementsByTagName("*");
for (var i = 0; i < items.length; i++) {
    // do stuff
}

I'm assuming the native option is faster. Wondering if there are other options I hadn't considered. Maybe a recursive option that iterates over child nodes in parallel.

nik7
  • 806
  • 3
  • 12
  • 20
kevzettler
  • 4,783
  • 15
  • 58
  • 103
  • 2
    Yes, the vanilla DOM way will be faster. But why do you need to iterate over _all_ the elements? – Matt Ball Jan 05 '12 at 17:44
  • 7
    cache your items.length so you're not calculating it every iteration of your loop, but yes, for loop with DOM call will be faster than .each – kinakuta Jan 05 '12 at 17:48
  • There shouldn't be an appreciable performance difference. jQuery will use `querySelectorAll` or something, assuming its available. Some subselector situations could cause it to not use native methods when it can but I don't think this is one of them, and as you've noted, you can code to ensure it does anyway. While there will some additional overhead, it won't be substantive compared to what you're already doing (looping through every element) in either situation. I would stick with jQuery unless you really don't care if it works in older IE browsers. – Jamie Treworgy Jan 05 '12 at 17:48
  • @jamietre `element.getElementsByTagName('*')` works in IE6 which is all jQuery supports as far back to as well. – Paul Jan 05 '12 at 17:51
  • Out of interest, why do you need to do it at all? – Dan Blows Jan 05 '12 at 18:03
  • @user1, learn something new every day, for some reason I thought it was broken in IE6. But looks like you're right. – Jamie Treworgy Jan 05 '12 at 18:04
  • 5
    @jamietre Also jQuery can't optimize "body *" very well. It does great with just "body" but it you use "body *" it uses sizzle JS. Which means it does eventually end up calling `document.querySelectorAll('body *')`, but it has to run about 200 lines of Javascript code first before it decides to do that, including a regex test and other things. That might not seem like a lot, but it comparison to `document.body.getElementsByTagName('*')` it is a lot. – Paul Jan 05 '12 at 18:13
  • Regarding why I need to. I have a case where I need to find all the elements on a page that have the css attribute position:fixed and act accordingly. Everything I found regarding this pointed at iterating over every element. I could probably have addressed the question in that regard or make a separate question for that. – kevzettler Jan 05 '12 at 18:15
  • Actually "body *" is faster (surprised me too): http://jsperf.com/js-vs-jquery-select-all .. also interesting that there is a difference of about an order of 2 between jQuery and the native method. This is probably because jQuery iterates through the entire list to create the result set internally, and then we do it again afterwards. In that sense, the DOM method would be faster. But that's got to do with result set building and not sizzle. – Jamie Treworgy Jan 05 '12 at 18:28
  • @kevzettler... is that really all you have to work with for this problem? You don't know: any containers that the "fixed" things could be within, any element types (e.g. its probably only `div` elements); you have no ability to assign classes or otherwise mark the things you're looking for other than to look for a style? Is this a real world situation or a hypothetical problem? – Jamie Treworgy Jan 05 '12 at 18:30
  • 3
    @jamietre I believe the reason you found jQuery to be faster is because you were using `document.querySelectorAll('*')` instead of `document.querySelectorAll('body *')` or `document.body.querySelectorAll('*')`. I updated your tests accordingly: http://jsperf.com/js-vs-jquery-select-all/2 – Paul Jan 05 '12 at 18:34
  • @jamietre I think you'll find `document.body.getElementsByTagName('*')` is the fastest. I'm not sure how jsperf works though, but I believe that since there is no body tag in your pasted HTML there will be no matches to "body *", because I'm pretty sure it runs in it's window object. – Paul Jan 05 '12 at 18:35
  • @user1 you're right (my test wasn't fair) but it was broken in favor of the Javascript code. Your version narrows the gap. There is a body tag for the tests, in the description it says the contents are put inside the body of a valid HTML5 document. But I think what's most important to note is the difference between the different jQuery ways of running the selector (and the js) - it's pretty negligble, so cleary none of them is using sizzle. If sizzle was invovled it would be a factor of 10 at least I'd think. – Jamie Treworgy Jan 05 '12 at 18:36
  • @jamietre Ah, I see that it is inserted into a body tag now. :) – Paul Jan 05 '12 at 18:38
  • 2
    @jamietre I am writing a 3rd party library that is included in users pages. I have no idea what their designs or markup look like. Our library injects a fixed element (a header bar) that may cause overlap with their designs. In order to prevent this we move any of their position:fixed elements accordingly. The only things I can assume is that they might have position:fixed elements within the body element. – kevzettler Jan 05 '12 at 18:39
  • @jamietre It does use sizzle though (for the `$('body *')` case). I used the uncompressed jQuery source with a breakpoint in the init function and then ran it and found that it went into Sizzle, but Sizzle is modified by jQuery if `if ( document.querySelectorAll )`, so in the end it just ends up using `context.querySelectorAll(query)` (where context === document, and query === 'body *'). – Paul Jan 05 '12 at 18:44
  • What I mean is it doesn't actually use sizzle to traverse the dom. Sure - there is more overhead- a lot of lines of code get run inside jQuery to do the same thing. But that's nothing compared to the time you'll spend iterating through the list of elements and checking classes, that's what I mean by negligible. – Jamie Treworgy Jan 05 '12 at 18:48
  • 1
    @kevzettler: elementFromPoint could help. If it's safe to assume the fixed position element will have the highest z-index (if not, well, that's just weird), and you can make a few other basic assumptions about a minimum size of any fixed element (e.g, it's at least 10 pixels high and 50 pixels wide) you could easily loop through a grid in the area you intend to occupy, and check only the styles of the unique elements that you found under each point in that grid. You'd be running a native JS method no more than (I would think) a few dozen times, probably way faster than selecting (esp with IE6) – Jamie Treworgy Jan 05 '12 at 18:53
  • 1
    Possible duplicate of: [How to loop through ALL DOM elements on a page?](https://stackoverflow.com/questions/4256339/javascript-how-to-loop-through-all-dom-elements-on-a-page) – hippietrail Jun 04 '16 at 06:53
  • 1
    check this **benchmark** http://jsben.ch/#/Ro9H6 – EscapeNetscape Nov 28 '16 at 12:45

6 Answers6

51

The Vanilla Javascript way you posted is the fastest. It will be faster than the jQuery solution you posted (See my comment on the question). If you're not removing or adding anything to the DOM in your loop and order of traversal doesn't matter, you can also speed it up ever so slightly by iterating in reverse:

var items = startElem.getElementsByTagName("*");
for (var i = items.length; i--;) {
    //do stuff
}

Edit: check this benchmark to see how much time you can save by using the native code: http://jsben.ch/#/Ro9H6

EscapeNetscape
  • 2,892
  • 1
  • 33
  • 32
Paul
  • 139,544
  • 27
  • 275
  • 264
  • I think you left out the conditional part of the for loop, but it looks like the code will actually just run forever, using i-- as the condition with no decrement. Is that correct? – Brian J Mar 14 '14 at 13:08
  • 15
    @BrianJ The `i--` is the conditional part of the loop. It's like saying `i-- != 0`, but since `0` is `falsy` you can leave that part off – Paul Mar 14 '14 at 15:34
  • var i = 0, items = startElem.getElementsByTagName("*"); while(items[i]) {//dostuf ; i++ } – Yuvaraj V Mar 03 '17 at 07:23
  • I'm curious why would backwards iterating can be faster. May you explain or point out to an article? – Lamar Jul 30 '17 at 12:49
  • @Lamar I don't think it is any faster anymore since JavaScript engines do a very good job of optimizing now. A long time ago it was every so slightly faster because instead of doing `i++` on every iteration and then not using the result of that expression and instead separately evaluating `i < length` for the loop condition, you could use `i--;` (effectively `i-- != 0`) both to advance the iteration and as the loop condition. – Paul Aug 08 '17 at 20:42
  • @Paulpro I believe the main reason has to do with the number of local variables. The decreasing iterator is faster because instead of the two variables (one for length, and one for index) involved in forwards iteration, there is only one variable involved in backwards iteration: the current index. If, however, Javascript had access to memory pointers like C/C++, then doing a forward iterator from the start memory address of the array to the end address should be even faster than backwards iteration because you would not have to add the start address to the index counter every iteration. – Jack G Nov 25 '18 at 16:48
16

UPDATE:

Don't use $('body *') to iterate over the elements. It will be much quicker to use $('*') if you go for the JQuery method (see comments for details).


Plain ol' JavaScript is much faster, relatively speaking.

Using a test fiddle, I get about 30ms to process 13000 elements with JQuery, and 8ms to process 23000 elements using JavaScript (both tested on Chrome):

JQuery:      433  elements/ms
JavaScript:  2875 elements/ms

Difference:  664% in favor of plain ol' JavaScript

Note: Unless you have an incredibly large amount of elements on your page, this isn't going to make much of a difference. Also, you probably should time the logic in your loop, as that might be the limiting factor in all this.

Update:

Here is the updated results when considering much more elements (about 6500 per loop), I get about 648000 elements in 1500ms with JQuery, and 658000 elements in 170ms with JavaScript. (both tested on Chrome):

JQuery:      432  elements/ms
JavaScript:  3870 elements/ms

Difference:  895% in favor of plain ol' JavaScript

Looks like JavaScript sped up while JQuery stayed about the same.

Briguy37
  • 8,342
  • 3
  • 33
  • 53
  • This is a pretty unfair test because you're only iterting through a handful of elements. Of course jQuery will get killed since it has significant overhead for invoking *any* method. As you increase the number of elements the difference will diminish to almost nothing. (See the jsperf tests user1 & myself linked to in comments) – Jamie Treworgy Jan 05 '12 at 18:56
  • @jamietre: Ok, please stand by while I up the number of elements. – Briguy37 Jan 05 '12 at 19:01
  • 3
    You are doing something in the jQuery loop that you aren't in the other one: converting each element to a jQuery object `var $this = $(this);`. Make the two iterators identical and I think you will find this difference largely vanishes. – Jamie Treworgy Jan 05 '12 at 19:18
  • (edit) you are also selecting "body \*" for jquery and just "*" for js. – Jamie Treworgy Jan 05 '12 at 19:24
  • @jamietre: If you don't have access to the element object, you can't perform any tasks. Thus, `var $this = $(this);` in some form is a required part of the JQuery test. I'm open to suggestions about different ways of getting a handle on the object, though. – Briguy37 Jan 05 '12 at 19:27
  • 2
    No, it's not. "this" is an element already in the loop... just like the one returned by the native method. Why would you have to convert it to a jQuery object in one situation, but not the other? They start out the same. – Jamie Treworgy Jan 05 '12 at 19:28
  • 2
    Here's a more fair test based on yours (using the same selector for both, and the same iteration method): http://jsfiddle.net/Pv8zm/4/ – Jamie Treworgy Jan 05 '12 at 19:29
  • @jamietre: Observe the same case with a selector of 'body *': http://jsfiddle.net/briguy37/Pv8zm/5/. Looks like that's what's making it so slow, thought the initialization doesn't help either, as you pointed out. – Briguy37 Jan 05 '12 at 19:42
  • If you change the JS to use `querySelectorAll('body *')` they're almost identical again. I am pretty sure that's the only method that jQuery uses, which probably explains the big difference seen in your last example. This is interesting because I wouldn't have guessed that using the element-targeted method as you did (`body.getElements...`) would be so much faster than `document.querySelectorAll` (at least in chrome as I've been running this). Of course it's a bit academic since one doesn't normally run the same selector a thousand times in a row :) – Jamie Treworgy Jan 05 '12 at 19:49
  • I've implemented @petars solution from above http://stackoverflow.com/a/8747184/93212 it seems significantly faster am I missing anything? – kevzettler Jan 05 '12 at 22:56
  • 2
    @kevzettler: I think that method iterates over text nodes as well, so it is inflating the number of nodes processed. See [this test](http://jsfiddle.net/briguy37/Pv8zm/9/). – Briguy37 Jan 06 '12 at 14:46
15

It's not a good idea generally but this should work:

function walkDOM(main) {
    var arr = [];
    var loop = function(main) {
        do {
            arr.push(main);
            if(main.hasChildNodes())
                loop(main.firstChild);
        }
        while (main = main.nextSibling);
    }
    loop(main);
    return arr;
}
walkDOM(document.body);

Not including textnodes:

function walkDOM(main) {
    var arr = [];
    var loop = function(main) {
        do {
            if(main.nodeType == 1)
                arr.push(main);
            if(main.hasChildNodes())
                loop(main.firstChild);
        }
        while (main = main.nextSibling);
    }
    loop(main);
    return arr;
}

Edited!

lithic
  • 181
  • 9
Petar Sabev
  • 848
  • 6
  • 12
  • 2
    I've taken this method and integrated it with some of the benchmarks below. It appears to be significantly faster. Am I missing anything? http://jsfiddle.net/Pv8zm/7/ – kevzettler Jan 05 '12 at 22:54
  • 1
    Can This example be updated to not include textnodes? The recursion is interesting but its iterating over unnecessary nodes – kevzettler Jan 06 '12 at 17:02
  • I think that the real time suck is going to come from checking the calculated CSS of every single node, not from the selector. But to skip text nodes just use `firstElementChild` and `nextElementSibling` instead. I don't think this is going to be substantively different from using a selector once you add the rest of the logic (e.g. this isn't what's making it slow). – Jamie Treworgy Jan 06 '12 at 22:46
7

The fastest way seems to be document.all (note that it's a property, not a method).

I've modified the fiddle of Briguy's answer to log these instead of jQuery, and it's consistently faster (than document.getElementsByTagName('*')).

The fiddle.

Camilo Martin
  • 37,236
  • 20
  • 111
  • 154
  • unfortunately it is not a standard property: https://developer.mozilla.org/en-US/docs/Mozilla/Mozilla_Web_Developer_FAQ#JavaScript_doesn.E2.80.99t_work.21_Why.3F – hoju Feb 15 '17 at 01:44
3

This is a solution to the problem as described in the comments (though not the actual question). I think it would be much faster the use elementFromPoint to test the area where you want to put your fixed-position element, and only worry about elements in that area. An example is here:

http://jsfiddle.net/pQgwE/4/

Basically, just set some minimum possible size of an element you're looking for, and scan the entire area that your new fixed position element wants to occupy. Build a list of unique elements found there, and only worry about checking the style of those elements.

Note that this technique assumes that the element you're looking for has the highest z-index (which seems a reasonable assumption for fixed position). If this is not good enough, then this could be adjusted to hide (or assign a minimum z-index) to each element after it's been discovered and test the point again, until nothing more is found (to be sure), and then restore them afterwards. This ought to happen so fast as to be imperceptible.

HTML:

<div style="position:fixed; left: 10px; top: 10px; background-color: #000000; 
    color: #FF0000;">I Am Fixed</div>
<div id="floater">OccupyJSFiddle!<br>for two lines</div>

JS:

var w = $(window).width(), h=$(window).height(),
    minWidth=10,
    minHeight=10, x,y;

var newFloat = $('#floater'), 
    maxHeight = newFloat.height(),
    el, 
    uniqueEls=[],
    i;

for (x=0;x<w;x+=minWidth) {
    for (y=0;y<h&& y<maxHeight;y+=minHeight) {
        el = document.elementFromPoint(x,y);
        if (el && $.inArray(el,uniqueEls)<0) {
            uniqueEls.push(el);
        }
    }
}
// just for the fiddle so you can see the position of the elements 
// before anything's done
// alert("click OK to move the floater into position.");
for (i=0;i<uniqueEls.length;i++) {
    el = $(uniqueEls[i]);
    if (el.css("position")==="fixed") {
        el.css("top",maxHeight+1);
    }
}

newFloat.css({'position': 'fixed',
             'top': 0,
             'left': 0});
Jamie Treworgy
  • 23,934
  • 8
  • 76
  • 119
  • 1
    thanks for this attempt. However I don't believe it will work in my case. I've updated your example to be more similar to what i'm seeing in production. http://jsfiddle.net/pQgwE/5/ Two fixed positioned elements at top:0px overlapping. also in my case there are multiple fixed elements in the external design. Some of them positioned relative to others so doint an elementFromPoint method could get out of hand quickly. – kevzettler Jan 05 '12 at 22:59
  • 1
    Unless you have two overlapping elements that actually take up exactly the same real estate, it will still work, see: http://jsfiddle.net/pQgwE/14/ -- the change you made turned the element that was supposed to be yours into something else. But if for some reason you expect your clients to have multiple fixed position elements in exactly the same real estate (which makes no sense, since they would interfere with each other) there's no reason you can't do what I already suggested above: after you detect each one, hide it or lower its z-index, and check again to see what was behind it. – Jamie Treworgy Jan 05 '12 at 23:28
  • btw i'm not sure what you mean by "elementFromPoint method could get out of hand quickly". It doesn't matter whether elements are positioned relatively, or anything else. That's what is so great about that method. It tells you what's at a specific point regardless of how it got there. What you do once you detect an element you need to move is exactly the same using this method, as it would be if you iterated through the entire DOM to find them. It's just a lot less work to find the stuff you're interested in this way. – Jamie Treworgy Jan 05 '12 at 23:33
  • 2
    I apologize. I played around with your example some more and it appears to do exactly what I need. I've updated the example to show the kind of layout i'm working with and it flows better than what i'd hope. http://jsfiddle.net/pQgwE/17/ – kevzettler Jan 06 '12 at 00:13
  • 3
    That looks pretty cool actually. It should be easy to adjust this logic to handle different situations - if you only have a titlebar-sized area to check, even checking like every other pixel (if you had to) is probably a good bit faster than iterating/checking calculated CSS through the whole DOM. But I bet every 10 px is plenty and should be virtually instantaneous. Let me know if you need any other help tweaking it. – Jamie Treworgy Jan 06 '12 at 14:06
1

Most efficient:

const allDom = document.all || document.querySelectorAll("*");
const len = allDom.length;
for(let i=0; i<len; i++){
    let a = allDom[i];
}

https://jsben.ch/FwPzW

Eylon Sultan
  • 936
  • 9
  • 16