A couple days before this post, I asked a question about iterating children of a node versus using querySelectorAll and one of the comments pointed out that the example I made wasn't completely fair because when I removed a class from each child element of the parent, I didn't first test that the child's classList contained the class to be removed.
I thought that I read (but cannot now locate the source) on MDN that this was not beneficial because the remove method would just repeat what the contains method does. My impression was that it was less efficient to do so, which, of course, differs from the comments to the mentioned question. The accepted answer to this 9+ year-old question, appears to be in agreement with what I thought I had read, stating that:
Explicitly checking is pointless. It is a waste of time and bloats your code.
However, I modified the JS Bench example (modified version https://jsben.ch/LSbrl) that was provided in a comment to my question to compare the following for that same example; and it appears that testing using contains before attempting remove is about three times quicker than just executing remove.
const c = p.children;
const l = c.length;
for (let i = 0; i < l; i++) {
c[i].classList.remove('sel');
}
// versus
const c = p.children;
const l = c.length;
for (let i = 0, cL; i < l; i++) {
cL = c[i].classList;
if( cL.contains('sel') ) cL.remove('sel');
}
My question is why? Does this indicate that the two methods do not use the same code, such that remove is not contains with the added step of removing the class if it is found, but contains uses other data to make the determination and in a quicker manner?
The accepted answer to my recent question states that querySelectorAll "Uses highly optimized selector matching ... ". Does contains make use of the same?
There appears to be a similar relationship between the element.matches() and element.closest() methods. I asked a question about two years ago also but didn't know enough to ask it correctly.
In many instances, I'd expect that it makes little difference and won't be noticeable, but it would be nice to understand a bit better. Perhaps I am just slow to catch on, but I would not have expected "if contains then remove" to be quicker than just remove, from reading the documentation on MDN. And it may be an instance of early optimization that is not necessary until an issue arises.
The implementation details for DOMTokenList.contains and DOMTokenList.remove are not made explicit int the DOM specification, so it really depends on the specific browser's implementation of the underlying logic. Since your benchmark produces similar results in both Firefox and Chrome, I decided to take a look at Firefox.
You stated:
I thought that I read (but cannot now locate the source) on MDN that this was not beneficial because the remove method would just repeat what the contains method does.
At least for Firefox, this is patently false. The logic for nsDOMTokenList::Contains is very straightforward:
bool nsDOMTokenList::Contains(const nsAString& aToken) {
const nsAttrValue* attr = GetParsedAttr();
return attr && attr->Contains(aToken);
}
However, the nsDOMTokenList::Remove method does not rely on that logic:
void nsDOMTokenList::Remove(const nsTArray<nsString>& aTokens,
ErrorResult& aError) {
CheckTokens(aTokens, aError);
if (aError.Failed()) {
return;
}
const nsAttrValue* attr = GetParsedAttr();
if (!attr) {
return;
}
RemoveInternal(attr, aTokens);
}
Most of the work is deferred to the nsDOMTokenList::RemoveInternal method, which is all but guaranteed to mutate the DOM:
void nsDOMTokenList::RemoveInternal(const nsAttrValue* aAttr,
const nsTArray<nsString>& aTokens) {
MOZ_ASSERT(aAttr, "Need an attribute");
RemoveDuplicates(aAttr);
nsAutoString resultStr;
for (uint32_t i = 0; i < aAttr->GetAtomCount(); i++) {
if (aTokens.Contains(nsDependentAtomString(aAttr->AtomAt(i)))) {
continue;
}
if (!resultStr.IsEmpty()) {
resultStr.AppendLiteral(" ");
}
resultStr.Append(nsDependentAtomString(aAttr->AtomAt(i)));
}
mElement->SetAttr(kNameSpaceID_None, mAttrAtom, resultStr, true);
}
A cursory look at the DOMTokenList implementation for the Blink rendering engine that is used by Chrome/Chromium indicates a similar implementation.
You would have to profile the browser code itself to say for sure, but this is most likely the biggest contributor to the performance difference that you're seeing.
The conclusion, however, remains: unless you're planning to call classList.remove() on thousands of elements of which you are unsure if the specified class is present, it is unlikely to make a noticeable difference whether or not you call classList.contains() first.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With