style.setProperty vs setAttribute
Recently, I encountered an interesting question: which is faster, element.style.setProperty(property, value)
or element.setAttribute('style', 'property: value')
? At first glance, the answer seems obvious. Logic suggests that setProperty
should store the value directly in the CSSOM, while setAttribute
first sets the style
attribute, which is then parsed into the CSSOM. Therefore, setProperty
should be faster. But is it really that straightforward? Let's delve into it.
To begin, let's refresh some foundational concepts. We know that styles are described using the CSS language. After receiving a string representation of styles in CSS, the browser parses it and constructs a CSSOM object. The interface of this object is defined by the specification https://www.w3.org/TR/cssom-1. It adheres to the principles of cascading and inheritance outlined in https://www.w3.org/TR/css-cascade-4.
From the specifications mentioned above, we know that the fundamental unit of CSS is a property
. Each property is assigned a value that is specific to that property. If a value is not explicitly specified, it is inherited from a parent style, or, if there is no parent, it will be set to the initial value.
The set of properties for an element is organized into CSSRule. There are different types of rules, with the most common type being the CSSStyleRule
, which defines the properties of an element. This type of rule begins with a valid selector followed by curly braces containing a set of properties and values, formatted as <selector>: { ... }
. Other types of rules exist as well, such as the CSSFontFaceRule
, which describes the parameters for an imported font using @font-face { ... }
, the CSSMediaRule
, represented as @media { ... }
, and others. You can find a complete list in the specification at https://www.w3.org/TR/cssom-1/#css-rules.
These rules are compiled into what is known as a CSSStyleSheet, which serves as an abstract representation of the <style>
tag. In turn, style sheets are aggregated into a collection and associated with the document.
All of these levels of abstraction constitute the CSS Object Model (CSSOM). While the specification primarily describes the syntax of the model, in practice, it also includes the direct implementation of the CSS API used by browsers. Functions such as computed style, color encoding methods, mathematical operations, and many others are also part of CSSOM.
Setting Properties in Blink
Having covered the theory, let's take a closer look under the hood of the browser, specifically at the Blink rendering engine used by browsers like Chromium, Opera, WebView, and others. Blink is part of the Chromium repository and is not supplied independently. Therefore, we will be conducting our experiments using Chromium (version 132.0.6812.1 as of the time this article was written).
style.setProperty
Let's begin with the method style.setProperty
.
element.style.setProperty("background-color", "red");
To understand what happens under the hood of Blink, it's essential to take a closer look at the inner workings of Blink itself.
/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#130
void AbstractPropertySetCSSStyleDeclaration::setProperty( const ExecutionContext* execution_context, const String& property_name, const String& value, const String& priority, ExceptionState& exception_state) { CSSPropertyID property_id = UnresolvedCSSPropertyID(execution_context, property_name); if (!IsValidCSSPropertyID(property_id) || !IsPropertyValid(property_id)) { return; } bool important = EqualIgnoringASCIICase(priority, "important"); if (!important && !priority.empty()) { return; } const SecureContextMode mode = execution_context ? execution_context->GetSecureContextMode() : SecureContextMode::kInsecureContext; SetPropertyInternal(property_id, property_name, value, important, mode, exception_state); }
First, a basic validity check is performed on the property that has been passed in. Then, the style's priority is evaluated, and an internal method SetPropertyInternal
is invoked.
/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#236
void AbstractPropertySetCSSStyleDeclaration::SetPropertyInternal( CSSPropertyID unresolved_property, const String& custom_property_name, StringView value, bool important, SecureContextMode secure_context_mode, ExceptionState&) { StyleAttributeMutationScope mutation_scope(this); WillMutate(); MutableCSSPropertyValueSet::SetResult result; if (unresolved_property == CSSPropertyID::kVariable) { AtomicString atomic_name(custom_property_name); bool is_animation_tainted = IsKeyframeStyle(); result = PropertySet().ParseAndSetCustomProperty( atomic_name, value, important, secure_context_mode, ContextStyleSheet(), is_animation_tainted); } else { result = PropertySet().ParseAndSetProperty(unresolved_property, value, important, secure_context_mode, ContextStyleSheet()); } if (result == MutableCSSPropertyValueSet::kParseError || result == MutableCSSPropertyValueSet::kUnchanged) { DidMutate(kNoChanges); return; } CSSPropertyID property_id = ResolveCSSPropertyID(unresolved_property); if (result == MutableCSSPropertyValueSet::kModifiedExisting && CSSProperty::Get(property_id).SupportsIncrementalStyle()) { DidMutate(kIndependentPropertyChanged); } else { DidMutate(kPropertyChanged); } mutation_scope.EnqueueMutationRecord(); }
Within this internal method, a mutation process is initiated, the string value is parsed, and this value is assigned to the specified property. Following this, the changes are reflected in the CSSOM. Overall, the process is straightforward.
setAttribute
Now, Let's Examine setAttribute.
element.setAttribute("style", "background-color: red");
This operation goes beyond the CSSOM and directly affects elements and their attributes, resulting in modifications to the DOM. Within the DOM, there is a class called Attr that describes a single attribute of an element. There are several ways to set the value of an attribute.
/third_party/blink/renderer/core/dom/attr.cc#73
void Attr::setValue(const AtomicString& value, ExceptionState& exception_state) { // Element::setAttribute will remove the attribute if value is null. DCHECK(!value.IsNull()); if (element_) { element_->SetAttributeWithValidation(GetQualifiedName(), value, exception_state); } else { standalone_value_or_attached_local_name_ = value; } }
This method allows us to assign a value to an existing attribute. In other words, we can do something like this.
element.style = "background-color: red";
This will lead to the invocation of the method SetAttributeWithValidation
, which we will discuss shortly.
However, in our original case, we are not directly accessing the style
attribute; instead, we are calling the setAttribute
method from the Element class.
/third_party/blink/renderer/core/dom/element.h#282
void setAttribute(const QualifiedName& name, const AtomicString& value) { SetAttributeWithoutValidation(name, value); }
This method, in turn, calls another method: SetAttributeWithoutValidation
.
So, what are the methods SetAttributeWithoutValidation
and SetAttributeWithValidation
?
/third_party/blink/renderer/core/dom/element.cc#10314
void Element::SetAttributeWithoutValidation(const QualifiedName& name, const AtomicString& value) { SynchronizeAttribute(name); SetAttributeInternal(FindAttributeIndex(name), name, value, AttributeModificationReason::kDirectly); } void Element::SetAttributeWithValidation(const QualifiedName& name, const AtomicString& value, ExceptionState& exception_state) { SynchronizeAttribute(name); AtomicString trusted_value(TrustedTypesCheckFor( ExpectedTrustedTypeForAttribute(name), value, GetExecutionContext(), "Element", "setAttribute", exception_state)); if (exception_state.HadException()) { return; } SetAttributeInternal(FindAttributeIndex(name), name, trusted_value, AttributeModificationReason::kDirectly); }
As we can see, both methods essentially perform the same function. They first synchronize the current value of the attribute to account for any pending mutations. After that, they proceed to set the new value. The only difference is that in the second case, the new value is validated against the specified attribute before it is set. In other words, the following is not allowed.
element.style = "invalid-prop: red" // <div style></div>
Attempting to assign an invalid style to the style
attribute will result in the attribute's value being removed.
However, there is nothing stopping us from doing this instead.
element.setAttribute("style", "invalid-prop: red") // <div style="invalid-prop: red"></div>
In this case, the style
will be set regardless of what we specified as the value.
This seems quite strange. Why not validate attribute values all the time? The answer is quite simple. The setAttribute
method acts as a sort of backdoor for attributes. It is designed to operate not only with built-in attributes but also with arbitrary ones, such as data-*.
element.setAttribute("data-ship-id", "324"); element.setAttribute("data-weapons", "laserI laserII"); element.setAttribute("data-shields", "72%"); element.setAttribute("data-x", "414354"); element.setAttribute("data-y", "85160"); element.setAttribute("data-z", "31940"); element.setAttribute("onclick", "spaceships[this.dataset.shipId].blasted()");
The setAttribute
method operates at the DOM element level, and its primary function is to assign a value to an attribute without any validations. A detailed algorithm is described in the DOM standard.
Which is Faster?
Based on the above information, we can speculate that style.setProperty
should be faster since, unlike setAttribute
, the engine doesn’t need to search for a reference to the attribute object in the attribute table and can directly proceed to set the value. On the other hand, the overhead of validating the value itself could be significant.
Test 1: One Property
To determine which method is actually faster, we will conduct an experiment. For this purpose, we will need an HTML page with a test element and a couple of buttons.
<div id="test-element"></div> <button onclick="handleSetProperty()">style.setProperty</button> <button onclick="handleSetAttribute()">setAttribute</button>
Additionally, a simple JavaScript script will be required.
const N = 100; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function createElement() { const el = document.getElementById("test-element"); const newEl = document.createElement("div"); newEl.setAttribute("id", "test-element"); newEl.setAttribute("width", 100); newEl.setAttribute("height", 100); el.replaceWith(newEl); return newEl; } async function handleSetProperty() { const durations = []; for await (const i of new Array(N).fill(true).map((_, index) => index)) { const el = createElement(); await sleep(300); const startTime = performance.now(); el.style.setProperty("background-color", "red"); const duration = performance.now() - startTime; durations.push(duration); await sleep(300); console.log(i); } console.log(durations); } async function handleSetAttribute() { const durations = []; for await (const i of new Array(N).fill(true).map((_, index) => index)) { const el = createElement(); await sleep(300); const startTime = performance.now(); el.setAttribute("style", "background-color: red"); const duration = performance.now() - startTime; durations.push(duration); await sleep(300); console.log(i); } console.log(durations); }
This script, upon clicking one of the buttons, will initiate a loop of 100 iterations that sets the value of the style for a test element. To ensure the integrity of the experiment, we will create a new test element during each iteration and insert pauses after creating the element and after setting the value. This approach aims to eliminate browser optimizations and guarantee the rendering of the element after each iteration. We will take measurements separately, following a hard refresh of the page.
After collecting the measurements from the 100 iterations, we will calculate the simple average to obtain the final result.
avgSetProperty 0.07400000274181366 avgSetAttribute 0.08999999761581420
In this experiment, style.setProperty
clearly takes the lead. However, we only attempted to set a single CSS property.
Test 2: Two Properties
For objectivity, we will repeat the experiment with two properties simultaneously. To facilitate this, we will make slight adjustments to the test functions.
async function handleSetProperty() { const durations = []; for await (const i of new Array(N).fill(true).map((_, index) => index)) { const el = createElement(); await sleep(300); const startTime = performance.now(); el.style.setProperty("background-color", "red"); el.style.setProperty("border", "1px solid blue"); const duration = performance.now() - startTime; durations.push(duration); await sleep(300); console.log(i); } console.log(durations); } async function handleSetAttribute() { const durations = []; for await (const i of new Array(N).fill(true).map((_, index) => index)) { const el = createElement(); await sleep(300); const startTime = performance.now(); el.setAttribute("style", "background-color: red; border: 1px solid blue;"); const duration = performance.now() - startTime; durations.push(duration); await sleep(300); console.log(i); } console.log(durations); }
Upon running the loops again, we obtained the following result.
avgSetProperty 0.10900000214576722 avgSetAttribute 0.10399999618530273
The situation has changed. The average speed of both methods has approximately equalized. The setAttribute
method is actually a little faster; however, this difference can be attributed to potential measurement inaccuracies. The reason for this is not even validation, as it is still insignificant at this point. In this case, another process comes into play: the mutation of the CSSStyleDeclaration object. This interface describes the structure of a set of styles, such as the body of the <style>
tag. A similar object also stores the style for an individual element. I previously provided a code snippet for the method that sets a property value in CSSStyleDeclaration
. Let's take another look at it.
/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#236
void AbstractPropertySetCSSStyleDeclaration::SetPropertyInternal( CSSPropertyID unresolved_property, const String& custom_property_name, StringView value, bool important, SecureContextMode secure_context_mode, ExceptionState&) { StyleAttributeMutationScope mutation_scope(this); WillMutate(); MutableCSSPropertyValueSet::SetResult result; if (unresolved_property == CSSPropertyID::kVariable) { AtomicString atomic_name(custom_property_name); bool is_animation_tainted = IsKeyframeStyle(); result = PropertySet().ParseAndSetCustomProperty( atomic_name, value, important, secure_context_mode, ContextStyleSheet(), is_animation_tainted); } else { result = PropertySet().ParseAndSetProperty(unresolved_property, value, important, secure_context_mode, ContextStyleSheet()); } if (result == MutableCSSPropertyValueSet::kParseError || result == MutableCSSPropertyValueSet::kUnchanged) { DidMutate(kNoChanges); return; } CSSPropertyID property_id = ResolveCSSPropertyID(unresolved_property); if (result == MutableCSSPropertyValueSet::kModifiedExisting && CSSProperty::Get(property_id).SupportsIncrementalStyle()) { DidMutate(kIndependentPropertyChanged); } else { DidMutate(kPropertyChanged); } mutation_scope.EnqueueMutationRecord(); }
First, the object is locked. Then the property value is parsed and saved. After this, the lock is released.
Here is how setting the style as a whole looks when using a text string.
/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#54
void AbstractPropertySetCSSStyleDeclaration::setCSSText( const ExecutionContext* execution_context, const String& text, ExceptionState&) { StyleAttributeMutationScope mutation_scope(this); WillMutate(); const SecureContextMode mode = execution_context ? execution_context->GetSecureContextMode() : SecureContextMode::kInsecureContext; PropertySet().ParseDeclarationList(text, mode, ContextStyleSheet()); DidMutate(kPropertyChanged); mutation_scope.EnqueueMutationRecord(); }
The same object is locked. Next, the entire text string is parsed and broken down into properties. The values are then saved, and the object is unlocked.
Thus, by calling style.setProperty
twice, we initiate the lock-parse-unlock process two times. In contrast, setAttribute
can parse the entire style in a single operation.
I must emphasize one particular point regarding style parsing optimization.
/third_party/blink/renderer/core/css/parser/css_parser_impl.cc#255
static ImmutableCSSPropertyValueSet* CreateCSSPropertyValueSet( HeapVector<CSSPropertyValue, 64>& parsed_properties, CSSParserMode mode, const Document* document) { if (mode != kHTMLQuirksMode && (parsed_properties.size() < 2 || (parsed_properties.size() == 2 && parsed_properties[0].Id() != parsed_properties[1].Id()))) { // Fast path for the situations where we can trivially detect that there can // be no collision between properties, and don't need to reorder, make // bitsets, or similar. ImmutableCSSPropertyValueSet* result = ImmutableCSSPropertyValueSet::Create(parsed_properties, mode); parsed_properties.clear(); return result; } ... }
The parser, after processing a string, produces an array of properties. However, this array may contain duplicate properties. To create a set of unique properties, it is necessary to iterate over the array again and gather the unique ones. However, if the original array contains only two properties, checking for uniqueness can be done simply with an if
statement by comparing them by their IDs. It may seem trivial, but it's quite satisfying.
Test 3: Multiple Properties
Let’s conduct another test. This time, we will use a larger number of properties — say, seven. Why seven specifically? This number is chosen for a reason, which I will explain shortly.
async function handleSetProperty() { const durations = []; for await (const i of new Array(N).fill(true).map((_, index) => index)) { const el = createElement(); await sleep(300); const startTime = performance.now(); el.style.setProperty("background-color", "red"); el.style.setProperty("border", "1px solid blue"); el.style.setProperty("position", "relative"); el.style.setProperty("display", "flex"); el.style.setProperty("align-items", "center"); el.style.setProperty("text-align", "center"); el.style.setProperty("text-transform", "uppercase"); const duration = performance.now() - startTime; durations.push(duration); await sleep(300); console.log(i); } console.log(durations); } async function handleSetAttribute() { const durations = []; for await (const i of new Array(N).fill(true).map((_, index) => index)) { const el = createElement(); await sleep(300); const startTime = performance.now(); el.setAttribute( "style", "background-color: red; border: 1px solid blue; position: relative; display: flex; align-items: center; text-align: center; text-transform: uppercase;", ); const duration = performance.now() - startTime; durations.push(duration); await sleep(300); console.log(i); } console.log(durations); }
The results of this test are quite logical.
avgSetProperty 0.17899999499320984 avgSetAttribute 0.12500000596046448
This time, setAttribute
achieved a clear victory in speed for the reasons mentioned above.
Test 4: Multiple Numeric Values
You may have noticed that in the previous test, all properties had string values. The reason for this is that numeric values coming from JavaScript do not require additional parsing from strings. This allows for further optimization and some savings in processing.
Unfortunately, there are not many properties that can handle numeric values in this way. Most of the properties we are familiar with that can accept numeric values are designed to work with various units of measurement, perform different mathematical operations, etc. As a result, it is necessary to convert their numeric values into other formats in any case. Therefore, developers have limited the set of properties that can accept numeric values. Currently, there are only seven such properties: opacity, fill-opacity, flood-opacity, stop-opacity, stroke-opacity, shape-image-threshold, and -webkit-box-flex.
Let’s repeat our previous experiment, but this time focusing solely on numeric properties. This is precisely why I selected only seven properties last time, allowing us to effectively compare the results.
async function handleSetProperty() { const durations = []; for await (const i of new Array(N).fill(true).map((_, index) => index)) { const el = createElement(); await sleep(300); const startTime = performance.now(); el.style.setProperty("opacity", 0.5); el.style.setProperty("fill-opacity", 0.5); el.style.setProperty("flood-opacity", 0.5); el.style.setProperty("stop-opacity", 0.5); el.style.setProperty("stroke-opacity", 0.5); el.style.setProperty("shape-image-threshold", 0.5); el.style.setProperty("-webkit-box-flex", 1); const duration = performance.now() - startTime; durations.push(duration); await sleep(300); console.log(i); } console.log(durations); } async function handleSetAttribute() { const durations = []; for await (const i of new Array(N).fill(true).map((_, index) => index)) { const el = createElement(); await sleep(300); const startTime = performance.now(); el.setAttribute( "style", "opacity: 0.5; fill-opacity: 0.5; flood-opacity: 0.5; stop-opacity: 0.5; stroke-opacity: 0.5; shape-image-threshold: 0.5; -webkit-box-flex: 1;", ); const duration = performance.now() - startTime; durations.push(duration); await sleep(300); console.log(i); } console.log(durations); }
avgSetProperty 0.17099999666213989 avgSetAttribute 0.09600000023841858
The setAttribute
method remains faster. Ultimately, the overhead of converting a string to a number does not compare to the performance cost of locking, unlocking, and writing a value. Nevertheless, in both cases, the results turned out to be slightly better than in Test 3.
Conclusion
Let’s summarize everything discussed above.
- The
style.setProperty
method demonstrates better performance when setting a single property. However, when two or more properties are set simultaneously,setAttribute
is faster. - The
setAttribute
method does not validate values, which can be both an advantage and a disadvantage. If it is important for us to ensure that only valid values are set, we can use the setterelement.style = "<style>"
. However, if we need to set a custom attribute on an element or an intentionally incorrect value,setAttribute
is indispensable. - There are seven properties that are allowed to accept numeric values from JavaScript scripts. Setting the values of these properties will be slightly faster due to the elimination of the string-to-number conversion operation.
EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru
Русская версия: https://blog.frontend-almanac.ru/style-setproperty-vs-setattribute