NodeIterator
(and TreeWalker
, for that matter) are almost never used, because of a variety of reasons. This means that information on the topic is scarce and answers like @gsnedders' come to be, which fail to mention any of the API's unique features. I know this question is almost a decade old, so excuse my necromancy.
1. Initiation & Performance
It is true that the initiation of a NodeIterator
is way slower than a method like querySelectorAll
, but that is not the performance you should be measuring.
The thing about NodeIterator
s is that they are live-ish in the way that, just like an HTMLCollection
or live NodeList
, you can keep using the object after initiating it once.
The NodeList
returned by querySelectorAll
is static and will have to be re-initiated every time you need to match newly added elements.
This version of the jsPerf puts the NodeIterator
in the preparation code. The actual test only tries to loop over all newly added elements with iter.nextNode()
. You can see that the iterator is now orders of magnitudes faster.
2. Selector performance
Okay, cool, caching the iterator after initiation makes this example faster than querying. How you use the API still matters for iteration speed, though. In this version, you can observe another significant difference. I've added 10 classes (done[0-9]
) that the selectors shouldn't be matching. The iterator loses about 10% of its speed, while the querySelectors lose 20%.
This version, on the other hand, shows what happens when you add another div >
at the start of the selector. The iterator loses 33% of its speed, while the querySelectors got a speed INCREASE of 10%.
Removing the initial div >
at the start of the selector like in this version shows that both methods become slower, because they match more than earlier versions. Like expected, the iterator is relatively more performant than the querySelectors in this case.
This means that filtering on basis of a node's own properties (its classes, attributes, etc.) is probably faster in a NodeIterator
, while having a lot of combinators (>, +, ~, etc.) in your selector probably means querySelectorAll
is faster.
This is especially true for the
(space) combinator. Selecting elements with querySelectorAll('article a')
is way easier than manually looping over all parents of every a
element, looking for one that has a tagName
of 'ARTICLE'
.
P.S. in §3.2, I give an example of how the exact opposite can be true if you want the opposite of what the space combinator does (exclude a
tags with an article
ancestor).
3. Impossible selectors
3.1 Simple hierarchical relationships
Of course, manually filtering elements gives you practically unlimited control. This means that you can filter out elements that would normally be impossible to match with CSS selectors. For example, CSS selectors can only "look back" in the way that selecting div
s that are preceded by another div
is possible with div + div
. Selecting div
s that are followed by another div
is impossible.
However, inside a NodeFilter
, you can achieve this by checking node.nextElementSibling.tagName === 'DIV'
. The same goes for every selection CSS selectors can't make.
3.2 More global hierarchical relationships
Another thing I personally love about the usage of NodeFilter
s, is that when passed to a TreeWalker
, you can reject a node and its whole sub-tree by returning NodeFilter.FILTER_REJECT
instead of NodeFilter.FILTER_SKIP
.
Imagine you want to iterate over all a
tags on the page, except for ones with an article
ancestor.
With querySelectors, you'd type something like
let a = document.querySelectorAll('a')
a = Array.prototype.filter.call(a, function (node) {
while (node = node.parentElement) if (node.tagName === 'ARTICLE') return false
return true
})
While in a NodeFilter
, you'd only have to type this
return node.tagName === 'ARTICLE' ? NodeFilter.FILTER_REJECT : // ✨ Magic happens here ✨
node.tagName === 'A' ? NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP
In conclusion
NodeIterator
s and TreeWalker
s shouldn't be instantiated for loads of loops and definitely shouldn't replace one-off loops. For all intents and purposes, they are just alternative methods for keeping track of lists/trees of nodes, with the latter also having a handy bit of sugar added with FILTER_REJECT
.
There's one main advantage both NodeIterator
s and TreeWalker
s have to offer:
- Live-ishness, as discussed in §1
However, don't use them when any of the following is true:
- Its instance is only going to be used once/a few times
- Complex hierarchical relationships are queried that are possible with CSS selectors
(i.e. body.no-js article > div > div a[href^="/"]
)