CSS Custom Properties performance in 2018
How are CSS Custom Properties performing in 2018?
CSS Custom Properties, aka CSS variables, have been available in stable Firefox since 2014 and Chrome since 2016. Despite its availability, its usage has not spread yet, and performance could be one of the reasons.
In particular, custom properties are like color
or font-size
, and they are inherited by children elements. Besides, it’s ubiquitous to set them in the root as follows:
:root { --color: red; }
button {
color: var(--color);
}
This CSS defines a --color
custom property to the document root, which is the html
element, and it will be inherited by all its nested children allowing any button
style to use it as variable.
Due to the impact of modifying a root custom property, there are valid performance issues to be kept in mind. Lisi Linhart wrote a detailed article about CSS custom variables performance in July 2017, so let’s see if things have changed meanwhile.
Style recalculation
<div class="container">
<span class="el"></span>
... 25000 more elements
</div>
.el {
background: var(--bg, orange);
}
Given a container
of 25000 span
nodes, the benchmark will set a --bg
property on the container and then use it for each child. With my current laptop, a 2018 MacBook Pro 15” 2,2 GHz Intel Core i7, I get a far worse result: 644ms (now) compared to 76ms (previous).
76ms seems to be too good though, and maybe it was an oversite by Lisi Linhart. Another benchmark by Matt Stow in 2017 returned a rendering time of 51ms for 1k items, whereas I get around 66ms for the same elements. Therefore I doubt it was possible to achieve 76ms with 25k spans.
Setting a custom property only on a single child hasn’t changed instead: 1.4ms (now) compared to 1.6ms (previous).
So it’s still clear that we must be careful with container custom properties because it affects children nodes and recalculation becomes expensive. If you use Custom Properties throughout your application and by defining them at the root element, you’ll incur significant performance issues.
However, this information alone doesn’t help with deciding whether to use them or not because other solutions which require children to change their styles are also not cheap. Let’s, for instance, compare it with inline styles, which is one of the strategies used in React for dynamic styling.
Try it yourself on Codepen, forked from the one by Lisi Linhart.
Results show how inline styles are actually slower than setting a custom property on the parent. You might therefore consider using CSS variables if you are currently relying on inline styles for dynamic styling.
Using calc()
The next case she tested was using CSS calc()
with different variations of the CSS variable with/without unit.
el.style = "transform: translateY(calc(var(--translation) * 1px))";
el.style = "transform: translateY(var(--translationPx))";
el.style = "transform: translateY(100px))";
el.style = "transform: translateY(calc(var(--translation) * 1%))";
el.style = "transform: translateY(var(--translationPercent))";
el.style = "transform: translateY(100%))";
The previous result, by Lisi Linhart:
Current result, on my machine:
The results seem to be consistent with the previous ones:
- As before, using static values like
100px
is the fastest option - Setting a CSS variable with unit like
--translationPx: 100px
is slower - Unitless CSS variables like
--translation: 100
is the slowest alternative
Performance appears to be almost the same as before, but here’s the oddity: if you try a similar test with 10k elements on Codepen, it will give completely different results.
The several ways of using calc
with CSS variables don’t have any actual performance difference! Why are the metrics contrasting on jsperf.org and Codepen?
The reason is that the jsperf test applies the CSS variable to each single node
for (var i = 0; i < testNodes.length; i++) {
testNodes[i].style = "..."
}
whereas the Codepen benchmark sets the property on the container element and the calculation is needed only once
container.style = "--translation: var(--yPercent);"
So how calc()
is used doesn’t make a difference if it’s set once in a parent container, otherwise you must be careful to repeat the operation on a large number of nodes. In both cases, using variables with units is preferable.
Setting CSS Variables with JS
Last test from Lisi Linhart: setting the custom property with JS in distinct ways.
testNodes[i].style.setProperty('--color', 'green');
testNodes[i].style = "--color: green";
testNodes[i].style = "color: green";
testNodes[i].style.setProperty('color', 'green');
We can conclude that el.setProperty('color', 'green')
is still the fastest option, but surprisingly el.setProperty('--color', 'green')
is faster than el.style = "color: green"
, which means that setProperty
is always more performant than inline styles, even when setting a custom property versus an inline hard-coded value. The reason could be that setting inline styles requires parsing the CSS.
So now we have ended the benchmarks done by Lisi Linhart, but we have one more from me.
CSS variables start-up performance
Since you cannot escape the time of the initial render, even if you never modify any custom property, let’s see how Chrome performs with loading a page of 25k items with and without custom properties:
/* With Custom Properties */
:root {
--bg: orange;
}
.el {
background: var(--bg);
}
/* Without Custom Properties */
.el {
background: orange;
}
The initial render with CSS variables is noticeable slower: 416ms vs 159ms. It’s not that bad though, but still to be kept in mind when using Custom Properties in a large application.
You might prefer to avoid shipping custom properties if you don’t need to change them at runtime. postcss-custom-properties supports the option preserve: false
to remove custom properties in compilation.
Conclusions
Compared to 2017, there have been some minor improvements with CSS Custom Properties performances. I definitely believe the time is mature to start using them in production, as more and more modern features like CSS Paint API will rely on them.
Nevertheless, we still have to be careful with their scope, by limiting usage of root properties, and we must not forget to measure the initial render timing.