译文链接
我的一个简短的回答是:能提高一点。虽然它可能还不足以在一般的 Web APP 中产生很大的差异,但我们值得去理解为什么它能提高样式性能。
首先,让我们回顾一下浏览器的渲染流程[2](rendering pipeline
),以及为什么我们可以推测 shadow DOM
能提高样式性能。我们知道,浏览器渲染过程的两个基本部分是样式计算(style calculation
)和布局计算(layout calculation
),或者简称为“样式”和“布局”。前者的工作是:确定哪些 DOM 节点具有哪些样式(基于 CSS),后者则是:确定这些 DOM 节点实际放置在页面上的位置(使用上一步中计算出的样式)。
Chrome DevTools 中的性能跟踪,显示了基本的 JavaScript → 样式(Style) → 布局(Layout) → 绘制(Paint)流程
浏览器很复杂,但一般来说,页面上的 DOM 节点和 CSS 规则越多,运行样式和布局步骤所需要的时间就越长。提高此过程性能的一个方法是将工作分解为更小的块,即封装(encapsulation
)——这是 shadow DOM 能提高样式性能的原因。
对于布局(Layout
)的封装,我们有 CSS Containment[3]。这个在其他文章[4]里已经讲过了,这里就不赘述了。可以这么说,我认为有足够的证据表明CSS containement
可以提高性能,所以如果你还没有尝试将contains: content
置放在 UI
里面以查看它是否可以提高布局性能,那么你绝对应该去试一试。
至于样式(Style
)的封装,我们有一些新奇玩意儿:shadow DOM[5]。就像 CSS Containment
能提高布局性能一样,shadow DOM
理论上 应该也能提高样式性能。让我们考虑一下原因:
什么是样式计算
如前文所述,样式计算不同于布局计算。布局计算与页面的几何布局有关,而样式计算则更明确地与 CSS 相关。这是采用如下规则的过程:
div > button {color: blue;}
还有一个 DOM 树:
<div><button></button></div>
...例子如上,我们可以确定<button>
应该具有color:blue
,因为它的父元素是 <div>
。粗略地说,这是计算 CSS 选择器(在本例中为 div > button
)的过程。
现在,在最坏的情况下,这是一个 时间复杂度为O(n * m)
操作,其中 n 是 DOM 节点的数量,m 是 CSS 规则的数量。(即遍历每个 DOM 节点和每个 CSS 规则,弄清楚它们是否相互匹配。)显然,浏览器不会这样做,否则任何大小合适的 Web 应用程序都会变得非常缓慢。浏览器在这方面有很多优化,这也是我们经常建议不要过分担心 CSS 选择器性能的部分原因。(请参阅这篇文章[6],了解该主题的最新处理)
也就是说,如果你使用过有着大量 CSS 代码量的代码库,你可能会注意到,在 Chrome 性能配置文件中,样式成本不是零。根据 CSS 的大小或复杂程度,你可能会发现实际上花在样式计算上的时间多于布局计算。因此,研究样式性能并不是完全没有价值的。
Shadow DOM 和样式计算
为什么shadow DOM
会提高样式性能?这里再说一次,是因为封装!如果你有一个包含 1,000 个规则的 CSS 文件和一个包含 1,000 个节点的 DOM 树,浏览器在运行前,并不会知道哪些规则适用于哪些节点。即便你使用 CSS Modules[7]、Vue scoped CSS[8] 或 Svelte scoped CSS[9] 来创作你的 CSS,最终你也只会得到一个隐式映射到 DOM 树的样式表,因此浏览器必须在运行时找出 CSS 和 DOM 树之间的联系(例如使用类或属性选择器)。Shadow DOM
则不同。使用 shadow 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></my-component></my-component>
在这种情况下,浏览器不需要针对 DOM 中的每个节点测试 div {color: green}
规则——它知道它的范围是 <my-component>
。<another-component>
中的div {color: blue}
规则同上。理论上,这可以加快样式计算过程,因为浏览器可以依赖于通过 shadow DOM 的显式范围,而不是通过类或属性的隐式范围。
基准测试
这是理论,但当然,在实践中事情总是更复杂。所以我整理了一个 benchmark[10] 来衡量shadow DOM
的样式计算性能。某些 CSS 选择器往往比其他选择器更快,因此为了获得不错的覆盖率,我测试了以下选择器:
- 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)
)
粗略地说,我希望 id
和 class
选择器是最快的,其次是attribute
和attribute value
选择器,然后是“silly”
选择器(添加一些东西以真正让样式引擎工作)。
为了测量,我使用了一个简单的 requestPostAnimationFrame[11] 插件,它可以测量在样式、布局和绘制上所花费的时间。这是 Chrome DevTools
中正在测量的内容的屏幕截图(注意 Timings
部分下的“total”
):
为了运行实际的基准测试,我使用了 Tachometer[12],这是一个很好的浏览器微基准测试工具。在这种情况下,我只取了 51 次迭代测试中的中位数。
基准测试创建了几个自定义元素,并且要么使用自己的 <style>
(shadow DOM “on”
)附加一个 shadow root
,要么使用具有隐式范围(shadow DOM “off”
)的全局 <style>
。通过这种方式,我想对 shadow DOM
本身和 shadow DOM “polyfills”
(即不依赖于 shadow DOM 的 CSS 作用域系统)进行公平的比较。
每个 CSS 规则看起来像这样:
#foo {color: #000000;}
每个组件的 DOM 结构看起来像这样
<div id="foo">hello</div>
(当然,对于属性和类选择器,DOM 节点将有一个属性或类。)
基准测试的结果
以下是 Chrome 中 1,000 个组件和每个组件 1 个 CSS 规则的结果:
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 |
如你所见,classes
和 id
选择器在打开或关闭 shadow DOM
时大致相同(实际上,没有 shadow DOM
更会快一点)。但是一旦选择器变得更复杂( attribute
、attribute value
和 “silly”
选择器),shadow DOM
就大致保持不变,而非 shadow DOM
版本时间消耗就更多。
如果我们将每个组件增加到 10 个 CSS 规则,我们可以更清楚地看到这种效果:
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 中也看到了相似的数字。这是具有 1,000 个组件和 1 个 CSS 规则的 Firefox:
id | class | attribute | attribute-value | silly | |
---|---|---|---|---|---|
Shadow DOM | 27 | 25 | 25 | 25 | 25 |
No Shadow DOM | 18 | 18 | 32 | 32 | 32 |
Firefox 有 1,000 个组件,每个组件有 10 条 CSS 规则:
id | class | attribute | attribute-value | silly | |
---|---|---|---|---|---|
Shadow DOM | 30 | 30 | 30 | 30 | 34 |
No Shadow DOM | 22 | 22 | 143 | 150 | 153 |
这是带有 1,000 个组件和 1 个 CSS 规则的 Safari:
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 条 CSS 规则:
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)。
结论和未来的工作
我们可以从这些数据中得出一些结论。首先,shadow DOM
确实可以提高样式性能,所以我们关于样式封装的理论是成立的。然而,id
和 classes
选择器足够快,以至于实际上是否使用 shadow DOM
并不重要——事实上,没有 shadow DOM
时它们甚至会稍微更快一点。这表明像 Svelte
、CSS Modules
或优秀的老式 BEM[13] 这样的系统正在使用性能方面的最佳方法。
这也表明,与 classes
相比,使用 attribute
进行样式封装不能很好地进行扩展。所以,也许像 Vue 这样的框架,切换到 classes
会更好。
另一个有趣的问题是,为什么在所有三个浏览器引擎中,classes
和 id
选择器在使用 shadow DOM
时都稍慢。这对浏览器供应商本身来说可能是一个更好的问题,我不会再进行推测。不过,我要说的是,这些差异的绝对值很小,我认为不值得偏向其中任何一个。数据中最清晰的信号是 shadow DOM
有助于保持样式成本大致恒定,而如果没有 shadow DOM
,你可应该坚持使用简单的选择器,如 classes
和 id
,以避免遇到性能悬崖。
至于未来的工作:这是一个非常简单的基准测试,有很多方法可以扩展它。例如,基准测试每个组件只有一个内部 DOM 节点,并且它只测试平面选择器——没有后代或兄弟选择器(例如 div div
、div > div
、div ~ div
和 div + div
)。理论上,这些场景也应该有利于 shadow DOM
,特别是因为这些选择器不能跨越 shadow
边界,所以浏览器不需要在 shadow root
之外寻找相关的祖先或兄弟姐妹。(尽管浏览器的 Bloom 过滤器[14]使这变得更加复杂 - 请参阅这些文档[15]以很好地解释这种优化是如何工作的。)
不过总的来说,我认为上述数字还没有大到足以让普通的网络开发者开始担心优化他们的 CSS 选择器,或者将他们的整个网络应用迁移到 shadow DOM
。这些基准测试结果可能仅在以下情况下才有意义:1) 你正在写一个框架,因此你选择的任何模式都会被放大数倍,或者 2) 你已经对你的 Web 应用程序进行了分析,并且看到了大量高额的样式计算成本。但是对于其他人,我希望你能懂得:至少这些结果是有趣的,它揭示了一些 shadow DOM
工作的原理。
更新:Thomas Steiner[16] 也想知道标签选择器的测试结果(例如 div {}
),所以我修改了基准测试[17]来测试它。我只会列出 Shadow DOM
版本的结果,因为基准测试使用 div
,在非 shadow DOM
情况下,不可能单独使用标签来区分不同的 div
。从绝对值来看,这些数字看起来非常接近 id
选择器和 classes
选择器的数字(甚至在 Chrome 和 Firefox 中更快一点):
Chrome | Firefox | Safari | |
---|---|---|---|
1,000 components, 1 rule | 53.9 | 19 | 56 |
1,000 components, 10 rules | 62.5 | 20 | 58 |