- Published on
译:CSS选择器性能的真相
- Authors
- Name
- Deng Hua
本文翻译至: The truth about CSS selector performance
目录
前置知识提要
编写 CSS 选择器的方式确实会影响浏览器呈现网页的方式。
每当页面的一部分发生更改时,运行它的浏览器引擎就需要查看新的DOM树,并根据新的CSS样式表确定如何更改其样式。这种将样式与DOM节点匹配的操作称为样式的重新计算。
style recalculation
在不涉及很多细节的情况下,浏览器引擎需要查看所有规则并决定哪些规则适用于给定元素。为此,引擎需要查看选择器规则,并且从右到左进行。
例如这样一段选择器: .wrapper .section .title .link
。 浏览器引擎会首先尝试将.link
与元素进行匹配,如果匹配,则从右到左沿着链向上查找找到一个具有.title
的祖先元素,然后找到一个具有.section
的祖先元素,最后一个具有.wrapper
的祖先元素。
此示例说明浏览器引擎仅匹配.link
较短的选择器比匹配较长的.wrapper .section .title .link
选择器更快,因为需要检查的步骤较少。
class并不是您可以在CSS选择器中使用的唯一标识符类型。一个有趣的例子是使用属性选择器并进行子字符串匹配,如[class*="icon-"]
。 这种类型的选择器要求浏览器引擎不仅要检查元素是否具有class属性,还要检查该属性的值是否包含子字符串"icon-"
。
说明了不同的选择器编写方式可能需要或多或少的工作来让引擎应用CSS规则。
在实践中,这重要吗?
或许。这在很大程度上取决于网页、DOM树的大小、CSS规则的数量以及DOM是否经常更改。不幸的是,对此没有任何规则。
事实上,谈到规则,作为一个行业,我们喜欢为好坏制定规则。规则帮助我们快速做出决策,并在编写代码和设计软件时指导我们。但它们也会让我们看不到具体案例中真正发生的事情。
在编写 CSS 选择器时,严格应用规则或使用 linter 自动执行,在某些情况下实际上可能会适得其反。
过于复杂的CSS选择器,再加上变化很大的巨大DOM树,很可能会导致性能不佳。但有一个平衡。仅仅为了取悦你的linter并希望获得更好的性能而对理论规则进行过度索引和更改选择器可能只会使你的CSS更难以阅读和维护,而没有多少实际收益。
因此,请以对您的应用程序有意义且易于阅读和维护的方式编写代码,然后测量重要用户场景的实际性能。
措施
测量您的关键应用程序场景,而不是盲目地应用一组如何编写快速代码的规则。了解您可以使用的工具并使用它们。
Microsoft Edge DevTools 有一个性能工具,当您的应用程序开始感觉缓慢时,它可以真正让您大开眼界。
这里我想强调一下“感觉”这个词。为您的用户建立同理心,并尽可能使用他们实际使用的设备。您的开发机器可能比用户的设备更强大。
事实上,使用 DevTools 可以做的一件好事就是直接从工具内减慢CPU和网络连接速度。
性能工具可能看起来相当复杂,但我们有文档可以提供帮助。此外,一切都只发生在您的浏览器中,因此您可以在不破坏任何内容的情况下进行尝试,并且如果遇到麻烦,您始终可以重新加载页面并重新打开 DevTools。学习使用可用的工具来衡量您的关键场景,并学习识别导致速度变慢的最大障碍。
如果样式重新计算确实是导致应用程序变慢的原因之一,那么我们有个好消息给您。当涉及到调查您关注的性能问题时,没有什么比拥有一个能够立即为您提供问题根本原因的工具更好的了。
更好的了解选择器统计数据
从Microsoft Edge 109开始,DevTools中的性能工具可以列出任何样式重新计算中成本最高的选择器。获取方法如下:
- 打开性能工具。
- 单击右上角的齿轮图标打开该工具的设置。
- 选中启用
Enable advanced rendering instrumentation (slow)
-高级渲染检测(慢速)选项。 - 单击“record”,在要改进的网页上执行场景,然后单击“stop”。
- 在记录的配置文件中,确定要改进的长样式重新计算,并在瀑布视图(“main”部分)中选择它。
- 在底部选项卡栏中,单击选择器统计信息。
DevTools会让浏览器引擎在style recalculation
样式重计算期间计算所有的CSS选择器。您可以按选择器处理时间或匹配次数对选择器进行排序。
如果您发现一个选择器需要很长时间处理并且匹配了很多次,那么它可能是一个值得尝试和改进的好候选者。选择器可以简化吗?是否可以使其更具体地描述其应匹配的元素?
这个新功能可以立即从可疑的样式重新计算转到导致其如此长的各个 CSS 选择器。然后,您可以返回源代码,改进这些特定的选择器,然后再次测量。
案例分析
为了让事情变得更实用,让我们尝试改进一个实际的网页。我们将使用为此目的构建的照片库页面作为演示。
该页面顶部有一个工具栏,可以按相机型号、光圈、曝光时间等过滤照片,并且现在在相机型号之间切换感觉有点慢。
尽管这个演示页面是专门为此构建的,但它确实展示了一个与我们在微软自己的产品中遇到的情况类似的案例。 Edge团队和Microsoft依赖Web平台的其他产品团队在这一领域密切合作,以创造最佳的用户体验。在某些特定场景中,我们在具有大量 DOM 元素的应用程序中看到异常长的样式重新计算(例如我们将在此处使用的演示页面,其中包含大约 5000 个元素)。访问 CSS 选择器统计工具对我们帮助很大。
我们将关注的场景如下:
- 加载演示页面,并等待过滤器准备就绪。
- 将相机型号过滤器切换到另一个值并开始性能记录。
- 切换回所有相机型号并停止记录。
切换回到所有照片的速度很慢,因此我们只测量该部分。我们还将 CPU 速度降低四倍,以获得比通常在性能更高的开发机器上获得的更真实的结果。
一旦记录准备好,我们可以很容易地在配置文件中看到一个很长的重新计算块,在我的例子中总计超过 900 毫秒的工作。让我们单击此块,打开选择器统计窗格,然后按经过的时间排序:
选择器需要匹配的工作越多,匹配的次数越多,我们通过改进该选择器获得的潜在胜利就越多。在上面的列表中,以下选择器看起来很有趣:
.gallery .photo .meta ::selection
.gallery .photo .meta li strong:empty
[class*=" gallery-icon--"]::before
.gallery .photo .meta li
*
html[dir="rtl"] .gallery .photo .meta li button
改进 ::selection 选择器
.gallery .photo .meta ::selection
我们在演示网页中使用.gallery .photo .meta ::selection
来设置页面照片元数据部分中用户选择的背景和文本颜色的样式。当用户选择照片下方的文本时,将使用自定义颜色。\
由于代码中的错误,这种特殊情况实际上是有问题的。选择器实际上应该是.gallery .photo .meta::selection
,而.meta
和::selection
之间应该没有多余的空格。
因为那一个空格,选择器实际上被引擎解释为:.gallery .photo .meta *::selection
,这使得在样式重新计算期间匹配速度变慢,因为引擎需要检查所有DOM元素,然后验证它们是否嵌套在正确的祖先中。 如果没有空格,引擎只需要在进一步检查之前检查元素是否具有.meta
类。
改进 :empty 选择器
.gallery .photo .meta li strong:empty
选择器.gallery.photo .meta li strong:empty
乍一看就很可疑。 :empty
伪选择器表示: 仅在strong
元素没有任何内容时匹配。
这需要引擎做更多的工作,而不仅仅是检查元素的标签名称,但这样的写法很常见且实用。
然而,看看与此规则接近的其他 CSS 规则,我们可以看到以下内容:
.gallery .photo .meta li strong:empty {
padding: .125rem 2rem;
margin-left: .125rem;
background: var(--dim-bg-color);
}
html[dir="rtl"] .gallery .photo .meta li strong:empty {
margin-left: unset;
margin-right: .125rem;
}
在第二个示例中,相同的选择器被重复使用了两次,但是第二个示例以html[dir=rtl]
作为前缀。这种做法在页面的文本方向为从右到左时非常有用,因为这样可以覆盖第一个规则。在这种情况下,右到左方向的规则将覆盖左边距,并将其替换为右边距。
为了改善这一点,我们可以使用 CSS 逻辑属性。我们可以使用适应任何文本方向的逻辑方向,而不是指定物理边距方向,如下所示:
.gallery .photo .meta li strong:empty {
padding: .125rem 2rem;
margin-inline-start: .125rem;
background: var(--dim-bg-color);
}
改进 [class*=" gallery-icon--"] 选择器
[class*=" gallery-icon--"]::before
属性选择器非常有用,因此在删除它们之前,请检查它们是否真的会产生负面影响。在我们的例子中,这个选择器似乎确实发挥了作用。以下是我们使用此选择器的 CSS 规则:ƒ
[class*=" gallery-icon--"]::before {
content: '';
display: block;
width: 1rem;
height: 1rem;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
filter: contrast(0);
}
.gallery-icon--camera::before { background-image: url(...); }
.gallery-icon--aperture::before { background-image: url(...); }
.gallery-icon--exposure::before { background-image: url(...); }
...
这里的想法是,我们可以将这些图标类中的任何一个分配给一个元素,它将获得相应的图标。
这要求引擎读取class
的值并对其进行子字符串比对。我们可以帮助引擎减少工作:
.gallery-icon::before {
content: '';
display: block;
width: 1rem;
height: 1rem;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
filter: contrast(0);
}
.gallery-icon.camera::before { background-image: url(...); }
.gallery-icon.aperture::before { background-image: url(...); }
.gallery-icon.exposure::before { background-image: url(...); }
现在,我们不再只使用一个类,而是需要向元素添加两个类:
<div class="gallery-icon camera">
而不是
<div class="gallery-icon--camera">
但总体而言,该功能仍然非常易于使用,并且当有许多 DOM 节点需要重新设置样式(如我们的演示页面中所示)时,引擎的工作量会减少。
改进 .gallery .photo .meta li 选择器
.gallery .photo .meta li
这个选择器强制浏览器去检查li
元素的祖先列表中的多个级别。我们知道网页有很多li
元素,这可能需要大量时间。
我们可以通过为li
元素指定一个特定的class,并删除不必要的嵌套来简化这一过程。例如:
.photo-meta {
display: flex;
align-items: center;
gap: .5rem;
height: 1.5rem;
}
改进 * 选择器
*
*
CSS选择器中用于匹配任何元素的通用选择器。这种匹配任何内容的能力意味着引擎需要将关联规则应用于所有元素。
正如我们在性能记录中看到的,这个选择器确实被匹配了很多次。
在我们的例子中,它应用了特定的“box-sizing”值:
* {
box-sizing: border-box;
}
这在 CSS 中很常见,但在我们的例子中,删除它实际上是有意义的,仅在需要的地方应用box-sizing
。
优化结果
完成所有这些改进后,是时候再次检查我们的场景的性能了。
在上面的性能记录中,相同的重新计算样式任务块运行时间几乎为一秒,现在运行时间约为 300 毫秒,这真是一个巨大的提升!
改进某些 CSS 选择器可以带来重要的性能提升。这将取决于您的特定用例。请使用性能工具测试网页的性能,如果您发现样式重计算导致场景变慢,请使用 Microsoft Edge 中的新选择器统计窗格。