Update: I wrote a follow-up post on this topic.
Short answer: Kinda. It depends. And it might not be enough to make a big difference in the average web app. But it’s worth understanding why.
First off, let’s review the browser’s rendering pipeline, and why we might even speculate that shadow DOM could improve its performance. Two fundamental parts of the rendering process are style calculation and layout calculation, or simply “style” and “layout.” The first part is about figuring out which DOM nodes have which styles (based on CSS), and the second part is about figuring out where to actually place those DOM nodes on the page (using the styles calculated in the previous step).

A performance trace in Chrome DevTools, showing the basic JavaScript → Style → Layout → Paint pipeline.
Browsers are complex, but in general, the more DOM nodes and CSS rules on a page, the longer it will take to run the style and layout steps. One of the ways we can improve the performance of this process is to break up the work into smaller chunks, i.e. encapsulation.
For layout encapsulation, we have CSS containment. This has already been covered in other articles, so I won’t rehash it here. Suffice it to say, I think there’s sufficient evidence that CSS containment can improve performance (I’ve seen it myself), so if you haven’t tried putting contain: content
on parts of your UI to see if it improves layout performance, you definitely should!
For style encapsulation, we have something entirely different: shadow DOM. Just like how CSS containment can improve layout performance, shadow DOM should (in theory) be able to improve style performance. Let’s consider why.
What is style calculation?
As mentioned before, style calculation is different from layout calculation. Layout calculation is about the geometry of the page, whereas style calculation is more explicitly about CSS. Basically, it’s the process of taking a rule like:
div > button { color: blue; }
And a DOM tree like:
<div> <button></button> </div>
…and figuring out that the <button>
should have color: blue
because its parent is a <div>
. Roughly speaking, it’s the process of evaluating CSS selectors (div > button
in this case).
Now, in the worst case, this is an O(n * m)
operation, where n
is the number of DOM nodes and m
is the number of CSS rules. (I.e. for each DOM node, and for each rule, figure out if they match each other.) Clearly, this isn’t how browsers do it, or else any decently-sized web app would become grindingly slow. Browsers have a lot of optimizations in this area, which is part of the reason that the common advice is not to worry too much about CSS selector performance (see this article for a good, recent treatment of the subject).
That said, if you’ve worked on a non-trivial codebase with a fair amount of CSS, you may notice that, in Chrome performance profiles, the style costs are not zero. Depending on how big or complex your CSS is, you may find that you’re actually spending more time in style calculation than in layout calculation. So it isn’t a completely worthless endeavor to look into style performance.
Shadow DOM and style calculation
Why would shadow DOM improve style performance? Again, it’s because of encapsulation. If you have a CSS file with 1,000 rules, and a DOM tree with 1,000 nodes, the browser doesn’t know in advance which rules apply to which nodes. Even if you authored your CSS with something like CSS Modules, Vue scoped CSS, or Svelte scoped CSS, ultimately you end up with a stylesheet that is only implicitly coupled to the DOM, so the browser has to figure out the relationship at runtime (e.g. using class or attribute selectors).
Shadow DOM is different. With shadow DOM, the browser doesn’t have to guess which rules are scoped to which nodes – it’s right there in the DOM:
<my-component> #shadow-root <style>div {color: green}</style> <div></div> <my-component> <another-component> #shadow-root <style>div {color: blue}</style> <div></div> </another-component>
In this case, the browser doesn’t need to test the div {color: green}
rule against every node in the DOM – it knows that it’s scoped to <my-component>
. Ditto for the div {color: blue}
rule in <another-component>
. In theory, this can speed up the style calculation process, because the browser can rely on explicit scoping through shadow DOM rather than implicit scoping through classes or attributes.
Benchmarking it
That’s the theory, but of course things are always more complicated in practice. So I put together a benchmark to measure the style calculation performance of shadow DOM. Certain CSS selectors tend to be faster than others, so for decent coverage, I tested the following selectors:
- ID (
#foo
) - class (
.foo
) - attribute (
[foo]
) - attribute value (
[foo="bar"]
) - “silly” (
[foo="bar"]:nth-of-type(1n):last-child:not(:nth-of-type(2n)):not(:empty)
)
Roughly, I would expect IDs and classes to be the fastest, followed by attributes and attribute values, followed by the “silly” selector (thrown in to add something to really make the style engine work).
To measure, I used a simple requestPostAnimationFrame
polyfill, which measures the time spent in style, layout, and paint. Here is a screenshot in the Chrome DevTools of what’s being measured (note the “total” under the Timings section):
To run the actual benchmark, I used Tachometer, which is a nice tool for browser microbenchmarks. In this case, I just took the median of 51 iterations.
The benchmark creates several custom elements, and either attaches a shadow root with its own <style>
(shadow DOM “on”) , or uses a global <style>
with implicit scoping (shadow DOM “off”). In this way, I wanted to make a fair comparison between shadow DOM itself and shadow DOM “polyfills” – i.e. systems for scoping CSS that don’t rely on shadow DOM.
Each CSS rule looks something like this:
#foo { color: #000000; }
And the DOM structure for each component looks like this:
<div id="foo">hello</div>
(Of course, for attribute and class selectors, the DOM node would have an attribute or class instead.)
Benchmark results
Here are the results in Chrome for 1,000 components and 1 CSS rule for each component:
Click for table
id | class | attribute | attribute-value | silly | |
---|---|---|---|---|---|
Shadow DOM | 67.90 | 67.20 | 67.30 | 67.70 | 69.90 |
No Shadow DOM | 57.50 | 56.20 | 120.40 | 117.10 | 130.50 |
As you can see, classes and IDs are about the same with shadow DOM on or off (in fact, it’s a bit faster without shadow DOM). But once the selectors get more interesting (attribute, attribute value, and the “silly” selector), shadow DOM stays roughly constant, whereas the non-shadow DOM version gets more expensive.
We can see this effect even more clearly if we bump it up to 10 CSS rules per component:
Click for table
id | class | attribute | attribute-value | silly | |
---|---|---|---|---|---|
Shadow DOM | 70.80 | 70.60 | 71.10 | 72.70 | 81.50 |
No Shadow DOM | 58.20 | 58.50 | 597.10 | 608.20 | 740.30 |
The results above are for Chrome, but we see similar numbers in Firefox and Safari. Here’s Firefox with 1,000 components and 1 rule each:
Click for table
id | class | attribute | attribute-value | silly | |
---|---|---|---|---|---|
Shadow DOM | 27 | 25 | 25 | 25 | 25 |
No Shadow DOM | 18 | 18 | 32 | 32 | 32 |
And Firefox with 1,000 components, 10 rules each:
Click for table
id | class | attribute | attribute-value | silly | |
---|---|---|---|---|---|
Shadow DOM | 30 | 30 | 30 | 30 | 34 |
No Shadow DOM | 22 | 22 | 143 | 150 | 153 |
And here’s Safari with 1,000 components and 1 rule each:
Click for table
id | class | attribute | attribute-value | silly | |
---|---|---|---|---|---|
Shadow DOM | 57 | 58 | 61 | 63 | 64 |
No Shadow DOM | 52 | 52 | 126 | 126 | 177 |
And Safari with 1,000 components, 10 rules each:
Click for table
id | class | attribute | attribute-value | silly | |
---|---|---|---|---|---|
Shadow DOM | 60 | 61 | 81 | 81 | 92 |
No Shadow DOM | 56 | 56 | 710 | 716 | 1157 |
All benchmarks were run on a 2015 MacBook Pro with the latest version of each browser (Chrome 92, Firefox 91, Safari 14.1).
Conclusions and future work
We can draw a few conclusions from this data. First off, it’s true that shadow DOM can improve style performance, so our theory about style encapsulation holds up. However, ID and class selectors are fast enough that actually it doesn’t matter much whether shadow DOM is used or not – in fact, they’re slightly faster without shadow DOM. This indicates that systems like Svelte, CSS Modules, or good old-fashioned BEM are using the best approach performance-wise.
This also indicates that using attributes for style encapsulation does not scale well compared to classes. So perhaps scoping systems like Vue would be better off switching to classes.
Another interesting question is why, in all three browser engines, classes and IDs are slightly slower when using shadow DOM. This is probably a better question for the browser vendors themselves, and I won’t speculate. I will say, though, that the differences are small enough in absolute terms that I don’t think it’s worth it to favor one or the other. The clearest signal from the data is just that shadow DOM helps to keep the style costs roughly constant, whereas without shadow DOM, you would want to stick to simple selectors like classes and IDs to avoid hitting a performance cliff.
As for future work: this is a pretty simple benchmark, and there are lots of ways to expand it. For instance, the benchmark only has one inner DOM node per component, and it only tests flat selectors – no descendant or sibling selectors (e.g. div div
, div > div
, div ~ div
, and div + div
). In theory, these scenarios should also favor shadow DOM, especially since these selectors can’t cross shadow boundaries, so the browser doesn’t need to look outside of the shadow root to find the relevant ancestors or siblings. (Although the browser’s Bloom filter makes this more complicated – see these notes for an good explanation of how this optimization works.)
Overall, though, I’d say that the numbers above are not big enough that the average web developer should start worrying about optimizing their CSS selectors, or migrating their entire web app to shadow DOM. These benchmark results are probably only relevant if 1) you’re building a framework, so any pattern you choose is magnified multiple times, or 2) you’ve profiled your web app and are seeing lots of high style calculation costs. But for everyone else, I hope at least that these results are interesting, and reveal a bit about how shadow DOM works.
Update: Thomas Steiner wondered about tag selectors as well (e.g. div {}
), so I modified the benchmark to test it out. I’ll only report the results for the Shadow DOM version, since the benchmark uses div
s, and in the non-shadow case it wouldn’t be possible to use tags alone to distinguish between different div
s. In absolute terms, the numbers look pretty close to those for IDs and classes (or even a bit faster in Chrome and Firefox):
Click for table
Chrome | Firefox | Safari | |
---|---|---|---|
1,000 components, 1 rule | 53.9 | 19 | 56 |
1,000 components, 10 rules | 62.5 | 20 | 58 |
Posted by dfkaye on August 16, 2021 at 11:46 AM
Nolan, Thank you for doing this work. I may have other feedback if it becomes, but first wish to ask a favor of clarification. When you write “10 rules each” does that mean 10 rules per selector, or 10 selectors per element?
Posted by Nolan Lawson on August 16, 2021 at 2:21 PM
10 rules per element. It might be easier to explain with an example, so here is what 10 class rules look like in the benchmark:
Posted by Florian Hehlen on August 19, 2021 at 11:13 PM
Nice write-up! I think your lead and conclusions are a bit skewed towards defending the frameworks when you only focus on the Id and class metrics. What is the cost of assignment of class and IDs in JS to accomplish the results one could get with more complex selectors on top of the CSS computation? Further, I think it’s great that web components allow developers to use the full strength of CSS instead of having to learn one more library and follow the convention of avoiding complex selectors.
Posted by Nolan Lawson on August 22, 2021 at 4:35 PM
I was focusing on style costs, not JS costs. But to be fair, yes, setting an ID or class has a cost, as opposed to just directly using a
div
selector from the shadow DOM. I think for a full accounting of shadow vs non-shadow costs, though, you’d also have to consider the cost ofattachShadow()
, the custom element constructor, etc. I’d be interested to see that!I totally agree with this. I was trying to be fair to both frameworks and shadow DOM, but maybe I gave the wrong impression. The data clearly shows that shadow DOM basically lets you use whatever CSS selectors you want, and the style costs are kept to a minimum. This is great for folks who just want to write CSS!
Posted by Nolan Lawson: “Does shadow DOM improve style performance?” - Nicolas Hoizey on September 2, 2021 at 5:56 AM
[…] https://nolanlawson.com/2021/08/15/does-shadow-dom-improve-style-performance/ […]
Posted by The Overflow #89: Passwords are dead! - Coduza - Blog on September 3, 2021 at 7:14 AM
[…] Does shadow DOM improve style performance? nolanlawson.comIs the speed gain worth using the shadow DOM regularly? Let’s find out. […]
Posted by Best Of Tech #36 - Atol Open Blog on September 28, 2021 at 7:15 AM
[…] puisqu’on parle web components, Nolan Lawson a fait un benchmark cet été qui montre que l’utilisation du Shadow DOM permet (contrairement à ce que beaucoup […]
Posted by Links on Performance V - CSS-Tricks on December 21, 2021 at 11:31 AM
[…] Does shadow DOM improve style performance? — Nolan Lawson covers how, because of the inherent encapsulation of the shadow DOM, the styling gets applied a bit faster than it would if those styling rules were relevant to the entire page. But as ever, it depends, and it turns out that classes and IDs are actually faster outside of it (?!), so if you just style with those, that’s the way to go. […]
Posted by Generating Web Components with JavaScript, from HTML Templates on April 8, 2022 at 8:43 AM
[…] Nolan Lawson tested the performance of style rendering of components with and without a Shadow Dom. You might be inclined to believe, as I was, that using styles in the Shadow Dom would be a performance hit. Turns out, at scale, it’s not. Because there’s a smaller scope to reference for applying…. […]