原文地址:nolanlawson.com/2021/08/15/…
原文作者:nolanlawson.com/
发布时间:2021年8月15日
简短的回答。算是吧。这要看情况。对于一般的Web应用来说,这可能不足以造成很大的差异。但值得理解的是,为什么。
首先,让我们回顾一下浏览器的渲染管道,以及为什么我们甚至可以推测阴影DOM可以提高其性能。渲染过程的两个基本部分是样式计算和布局计算,或者简单说是 "样式 "和 "布局"。第一部分是计算出哪些DOM节点有哪些样式(基于CSS),第二部分是计算出这些DOM节点在页面上的实际位置(使用上一步计算的样式)。
Chrome DevTools中的性能跟踪,显示了基本的JavaScript → 样式 → 布局 → 涂装管道。
浏览器很复杂,但一般来说,页面上的DOM节点和CSS规则越多,运行样式和布局步骤的时间就越长。我们可以提高这个过程的性能的方法之一是将工作分成小块,即封装。
对于布局的封装,我们有CSS遏制。这一点在其他文章中已经介绍过了,所以我就不在这里重述了。我只想说,我认为有足够的证据表明,CSS封装可以提高性能(我自己也看到了),所以如果你还没有尝试在你的UI的部分内容上添加contain: 内容,看看它是否能提高布局性能,你一定要试试
对于样式封装,我们有完全不同的东西:阴影DOM。就像CSS封装可以提高布局性能一样,阴影DOM(理论上)也应该能够提高样式性能。让我们考虑一下原因。
什么是样式计算?
如前所述,样式计算与布局计算不同。布局计算是关于页面的几何形状的,而样式计算则更明确地涉及到CSS。基本上,它是取一个规则的过程,比如。
div > button {
color: blue;
}
和一个DOM树一样。
<div>
<button></button>
</div>
...然后发现<button>
应该有颜色:蓝色,因为它的父体是<div>
。粗略地说,这是评估CSS选择器的过程(本例中为div > button)。
现在,在最坏的情况下,这是一个O(n * m)
操作,其中n是DOM节点的数量,m是CSS规则的数量。(也就是说,对于每个DOM节点和每个规则,要弄清楚它们是否相互匹配)。显然,这不是浏览器的工作方式,否则任何规模适当的网络应用都会变得非常缓慢。浏览器在这方面有很多优化,这也是人们普遍建议不要太担心CSS选择器性能的部分原因(见本文对这一主题的良好、最新的处理)。
也就是说,如果你在一个非微不足道的代码库上工作,并有相当数量的CSS,你可能会注意到,在Chrome的性能配置文件中,样式成本并不是零。取决于你的CSS有多大或多复杂,你可能会发现你实际上在样式计算上花费的时间比在布局计算上花费的时间多。因此,研究样式性能并不是一个完全没有价值的努力。
阴影DOM和样式计算
为什么阴影DOM会提高样式性能?还是那句话,这是因为封装的关系。如果你有一个包含1000条规则的CSS文件,和一个包含1000个节点的DOM树,浏览器并不能事先知道哪些规则适用于哪些节点。即使你用CSS模块、Vue范围内的CSS或Svelte范围内的CSS来编写你的CSS,最终你的样式表也只是隐式地与DOM耦合,所以浏览器必须在运行时弄清这种关系(例如使用类或属性选择器)。
影子DOM则不同。有了影子DOM,浏览器不必猜测哪些规则适用于哪些节点,它就在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>
在这种情况下,浏览器不需要针对DOM中的每个节点测试div {color: green}
规则--它知道它的范围是<my-component>
。对于<another-component>
中的div {color: blue}
规则也是如此。理论上,这可以加快样式计算过程,因为浏览器可以通过影子DOM依赖显式范围,而不是通过类或属性依赖隐式范围。
对其进行基准测试
这就是理论,但当然在实践中事情总是更复杂。所以我做了一个基准来衡量阴影DOM的样式计算性能。某些CSS选择器往往比其他的更快,所以为了达到适当的覆盖率,我测试了以下选择器。
- ID (
#foo
) - 类(
.foo
) - 属性(
[foo]
) - 属性值(
[foo="bar"]
) - "傻"(
[foo="bar"]:nth-of-type(1n):last-child:not(:nth-of-type(2n)):not(:empty)
)
粗略地说,我希望ID和类是最快的,其次是属性和属性值,然后是 "愚蠢的 "选择器(为了增加一些东西以真正使样式引擎工作而扔进去)。
为了测量,我使用了一个简单的requestPostAnimationFrame
polyfill,它可以测量在样式、布局和画图中花费的时间。下面是Chrome DevTools的截图,显示了正在测量的内容(注意时间部分下的 "总数")。
为了运行实际的基准测试,我使用了Tachometer,它是一个很好的浏览器微观基准测试工具。在这种情况下,我只是取了51次迭代的中位数。
该基准创建了几个自定义元素,并且要么用自己的<style>
(shadow DOM "on")附加一个影子根,要么使用一个隐含范围的全局<style>
(shadow DOM "off")。通过这种方式,我想在shadow DOM本身和shadow DOM "polyfills "之间做一个公平的比较--即不依赖shadow DOM的CSS范围系统。
每条CSS规则看起来像这样。
#foo {
color: #000000;
}
而每个组件的DOM结构看起来是这样的。
<div id="foo">hello</div>
(当然,对于属性和类选择器,DOM节点会有一个属性或类来代替。)
基准结果
下面是Chrome浏览器中1000个组件和每个组件的1条CSS规则的结果。
含有1000个组件和1条规则的Chrome浏览器的图表。完整数据见表格
/ | 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 |
正如你所看到的,在影子DOM开启或关闭的情况下,类和ID是差不多的(事实上,在没有影子DOM的情况下会更快一点)。但是一旦选择器变得更加有趣(属性、属性值和 "愚蠢的 "选择器),阴影DOM就会保持大致不变,而非阴影DOM版本则变得更加昂贵。
如果我们把每个组件的CSS规则增加到10条,我们可以更清楚地看到这种效果。
/ | 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 |
以上是Chrome浏览器的结果,但我们在Firefox和Safari浏览器中也看到类似的数字。下面是Firefox的1000个组件和每个组件1条规则。
/ | id | class | attribute | attribute-value | silly |
---|---|---|---|---|---|
Shadow DOM | 27 | 25 | 25 | 25 | 25 |
No Shadow DOM | 18 | 18 | 32 | 32 | 32 |
火狐浏览器有1000个组件,每个组件有10条规则。
/ | id | class | attribute | attribute-value | silly |
---|---|---|---|---|---|
Shadow DOM | 30 | 30 | 30 | 30 | 34 |
No Shadow DOM | 22 | 22 | 143 | 150 | 153 |
这里是Safari浏览器,有1000个组件,每个组件有1条规则。
/ | id | class | attribute | attribute-value | silly |
---|---|---|---|---|---|
Shadow DOM | 57 | 58 | 61 | 63 | 64 |
No Shadow DOM | 52 | 52 | 126 | 126 | 177 |
Safari有1,000个组件,每个组件有10条规则。
/ | id | class | attribute | attribute-value | silly |
---|---|---|---|---|---|
Shadow DOM | 60 | 61 | 81 | 81 | 92 |
No Shadow DOM | 56 | 56 | 710 | 716 | 1157 |
所有的基准都是在2015年的MacBook Pro上运行的,每个浏览器都是最新版本(Chrome 92, Firefox 91, Safari 14.1)。
结论和未来工作
我们可以从这些数据中得出一些结论。首先,影子DOM确实可以提高样式性能,所以我们关于样式封装的理论是成立的。然而,ID和类选择器的速度足够快,实际上,是否使用影子DOM并不重要--事实上,在没有影子DOM的情况下,它们的速度略快。这表明像Svelte、CSS Modules或老式的BEM这样的系统在性能上使用的是最佳方法。
这也表明,与类相比,使用属性进行样式封装并不能很好地扩展。因此,也许像Vue这样的范围界定系统最好改用类。
另一个有趣的问题是,为什么在所有三个浏览器引擎中,在使用影子DOM时,类和ID的速度略慢。这可能是浏览器供应商自己的问题,我就不猜测了。不过,我想说的是,这些差异的绝对值足够小,我认为不值得偏向其中之一。数据中最清晰的信号是,阴影DOM有助于保持样式成本大致不变,而如果没有阴影DOM,你会想坚持使用简单的选择器,如类和ID,以避免撞上性能悬崖。
至于未来的工作:这是一个相当简单的基准,有很多方法可以扩展它。例如,该基准每个组件只有一个内部DOM节点,而且它只测试平坦的选择器--没有后代或同辈选择器(例如,div div
、div > div
、div ~ div
和div + div
)。理论上,这些情况也应该有利于影子DOM,特别是因为这些选择器不能跨越影子边界,所以浏览器不需要在影子根之外寻找相关的祖先或同辈。(尽管浏览器的布隆过滤器让这个问题变得更加复杂--请看这些注释,它很好地解释了这个优化的工作原理。)
不过总的来说,我认为上述数字还没有大到足以让普通的网络开发者开始担心优化他们的CSS选择器,或者将他们的整个网络应用迁移到阴影DOM。这些基准结果可能只有在以下情况下才有意义:1)你正在建立一个框架,所以你选择的任何模式都会被放大数倍,或者2)你已经对你的网络应用进行了分析,并且看到了很多高的样式计算成本。但是对于其他人来说,我希望至少这些结果是有趣的,并且揭示了一些关于阴影DOM的工作原理。
更新:Thomas Steiner也想知道标签选择器(例如div {}
),所以我修改了基准来测试它。我只报告影子DOM版本的结果,因为该基准使用div,而在非影子的情况下,不可能仅仅使用标签来区分不同的div。从绝对值来看,这些数字看起来与ID和类的数字相当接近(甚至在Chrome和Firefox中还会快一点)。
Chrome | Firefox | Safari | |
---|---|---|---|
1,000 components, 1 rule | 53.9 | 19 | 56 |
1,000 components, 10 rules | 62.5 | 20 | 58 |