diff --git a/dom/base/nsDOMTokenList.cpp b/dom/base/nsDOMTokenList.cpp index 87fb11e8c7..02acabacac 100644 --- a/dom/base/nsDOMTokenList.cpp +++ b/dom/base/nsDOMTokenList.cpp @@ -127,7 +127,29 @@ void nsDOMTokenList::AddInternal(const nsAttrValue* aAttr, const nsTArray& aTokens) { - if (!mElement) { + if (!mElement || aTokens.IsEmpty()) { + return; + } + + // Hot path: single-token classList.add() when the class is already present. + // Skip attribute rebuild and SetAttr entirely (avoids mutation observers / + // style invalidation). Frameworks hit this constantly. + if (aTokens.Length() == 1) { + const nsString& token = aTokens[0]; + if (aAttr && aAttr->Contains(token)) { + return; + } + + nsAutoString resultStr; + if (aAttr) { + aAttr->ToString(resultStr); + if (!resultStr.IsEmpty() && + !nsContentUtils::IsHTMLWhitespace(resultStr.Last())) { + resultStr.Append(' '); + } + } + resultStr.Append(token); + mElement->SetAttr(kNameSpaceID_None, mAttrAtom, resultStr, true); return; } @@ -161,6 +183,11 @@ nsDOMTokenList::AddInternal(const nsAttrValue* aAttr, addedClasses.AppendElement(aToken); } + // No new tokens: leave the attribute untouched (do not create class=""). + if (!oneWasAdded) { + return; + } + mElement->SetAttr(kNameSpaceID_None, mAttrAtom, resultStr, true); } @@ -190,20 +217,37 @@ nsDOMTokenList::RemoveInternal(const nsAttrValue* aAttr, { MOZ_ASSERT(aAttr, "Need an attribute"); + if (aTokens.IsEmpty()) { + return; + } + + // Hot path: single-token classList.remove() when the class is absent. + if (aTokens.Length() == 1 && !aAttr->Contains(aTokens[0])) { + return; + } + nsAutoString input; aAttr->ToString(input); WhitespaceTokenizer tokenizer(input); nsAutoString output; + bool removed = false; while (tokenizer.hasMoreTokens()) { auto& currentToken = tokenizer.nextToken(); - if (!aTokens.Contains(currentToken)) { - if (!output.IsEmpty()) { - output.Append(char16_t(' ')); - } - output.Append(currentToken); + if (aTokens.Contains(currentToken)) { + removed = true; + continue; } + if (!output.IsEmpty()) { + output.Append(char16_t(' ')); + } + output.Append(currentToken); + } + + // Token set unchanged: skip SetAttr to avoid style/mutation work. + if (!removed) { + return; } mElement->SetAttr(kNameSpaceID_None, mAttrAtom, output, true); @@ -302,6 +346,16 @@ nsDOMTokenList::ReplaceInternal(const nsAttrValue* aAttr, const nsAString& aToken, const nsAString& aNewToken) { + // Replacing a token with itself cannot change the token set. + if (aToken.Equals(aNewToken)) { + return; + } + + // Old token absent: no update (and avoid rewriting whitespace). + if (!aAttr->Contains(aToken)) { + return; + } + nsAutoString attribute; aAttr->ToString(attribute); diff --git a/js/src/jsarray.cpp b/js/src/jsarray.cpp index 7fa01b93a4..6f646b3b2d 100644 --- a/js/src/jsarray.cpp +++ b/js/src/jsarray.cpp @@ -1221,7 +1221,12 @@ js::array_join(JSContext* cx, unsigned argc, Value* vp) sepstr = cx->names().comma; } - // Step 6 is implicit in the loops below. + // Step 6: empty arrays always join to the empty string. + // (Separator ToString above still runs for side effects / OOM.) + if (length == 0) { + args.rval().setString(cx->names().empty); + return true; + } // An optimized version of a special case of steps 7-11: when length==1 and // the 0th element is a string, ToString() of that element is a no-op and @@ -1381,8 +1386,8 @@ template DenseElementResult ArrayReverseDenseKernel(JSContext* cx, HandleObject obj, uint32_t length) { - /* An empty array or an array with no elements is already reversed. */ - if (length == 0 || GetBoxedOrUnboxedInitializedLength(obj) == 0) + /* Empty, singleton, or uninitialized arrays are already reversed. */ + if (length <= 1 || GetBoxedOrUnboxedInitializedLength(obj) == 0) return DenseElementResult::Success; if (Type == JSVAL_TYPE_MAGIC) { @@ -1451,6 +1456,12 @@ js::array_reverse(JSContext* cx, unsigned argc, Value* vp) if (!GetLengthProperty(cx, obj, &len)) return false; + // length 0/1: reverse is a no-op; return this immediately. + if (len <= 1) { + args.rval().setObject(*obj); + return true; + } + if (!ObjectMayHaveExtraIndexedProperties(obj)) { ArrayReverseDenseKernelFunctor functor(cx, obj, len); DenseElementResult result = CallBoxedOrUnboxedSpecialization(functor, obj); diff --git a/js/src/jsstr.cpp b/js/src/jsstr.cpp index aa0a503f72..5209e979de 100644 --- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -2329,6 +2329,12 @@ js::str_startsWith(JSContext* cx, unsigned argc, Value* vp) // Step 13 uint32_t searchLen = searchStr->length(); + // Empty search string always matches (and is extremely common). + if (searchLen == 0) { + args.rval().setBoolean(true); + return true; + } + // Step 14 if (searchLen + start < searchLen || searchLen + start > textLen) { args.rval().setBoolean(false); @@ -2395,6 +2401,12 @@ js::str_endsWith(JSContext* cx, unsigned argc, Value* vp) // Step 13 uint32_t searchLen = searchStr->length(); + // Empty search string always matches. + if (searchLen == 0) { + args.rval().setBoolean(true); + return true; + } + // Step 15 (reordered) if (searchLen > end) { args.rval().setBoolean(false);