探究 CSS 选择器的性能真相

开发 前端
改进某些 CSS 选择器可以带来重要的性能提升。然而,关键是要记住,这主要取决于特定用例。可以使用性能工具来测试网页的性能,如果发现样式重新计算使渲染变慢,就使用 Edge 浏览器中新的“选择器统计信息”功能!

大家好,我是 CUGGZ。

在 CSS 中,有些选择器会比其他选择器执行速度更快。下面就来深入研究 CSS 选择器的性能真相,看看如何编写 CSS 选择器才能更快地执行!

幕后

编写 CSS 选择器的方式会影响浏览器如何渲染页面。每当页面发生变化时,运行它的浏览器引擎需要查看新的 DOM 树,并确定如何根据可用的 CSS 样式表对其进行样式设置。这种将样式与 DOM 节点匹配的操作称为重新计算样式。浏览器引擎需要查看所有规则并决定哪些规则适用于给定元素。为此,引擎需要从右向左查看选择器规则。

例如,当引擎看到像.wrapper .section .title .link这样的选择器时,它会首先尝试将link类与元素匹配,如果匹配,则从右到左沿链向上找到类名为title的祖先元素,然后找到类名为section的元素,最后找到类名为wrapper的元素。

这个例子说明,浏览器引擎只匹配 .link 可能比匹配更长的 .wrapper .section .title .link 选择器更快,因为需要检查的更少了。

当然,类并不是在 CSS 选择器中可以使用的唯一类型标识符。一个有趣的例子就是使用属性选择器并进行子字符串匹配,如 [class*="icon-"],这选择器就要求浏览器引擎不仅要检查元素是否有 class 属性,还要检查这个属性的值是否包含子字符串 icon-。这个例子说明,不同的选择器编写方式可能需要引擎或多或少的工作来应用 CSS 规则。

在实践中,这重要吗?

这在很大程度上取决于网页、DOM 树的大小、CSS 规则的数量以及 DOM 是否经常变化。不幸的是,并没有关于这方面的规则。

事实上,谈到规则,我们喜欢为什么是好的和什么是坏的制定规则。规则帮助我们快速做出决定,并在编写代码和设计软件时提供指导。但这也会让我们无法了解具体情况下真正发生的事。

在编写 CSS 选择器时,严格应用规则或使用 linter 自动执行,在某些情况下可能会适得其反。过于复杂的 CSS 选择器,再加上变化很大的巨大 DOM 树,很可能会导致性能不佳。过度的套用理论规则来获得更好的性能,可能会使 CSS 更难阅读和维护,并且实际收益并不大。

因此,尽可能以对应用有意义且易于阅读和维护的方式来编写代码,然后再去衡量重要用户场景的实际性能。

性能测量工具

Edge 浏览器中的 DevTools 提供了一个性能工具,它可以帮助我们测量页面性能。在实际的测试中,我们要为用户建立同理心,并尽可能使用他们实际使用的设备。因为往往开发机器可能比用户的设备强大得多。DevTools 可以直接从工具内部降低 CPU 和网络连接速度。

图片

从 Edge 109 版本开始,DevTools 中的性能工具可以列出样式重新计算中成本最高的选择器。使用方法如下:

  1. 打开 DevTools 中的性能工具;
  2. 点击右上角的齿轮图标打开工具设置。
  3. 选中Enable advanced rendering instrumentation (slow)

图片

  1. 点击录制按钮,在要改进的网页上执行特定场景,然后单击停止;
  2. 在记录的配置文件中,确定要改进的重新计算样式,并在瀑布视图(“主要”部分)中选择它;
  3. 在底部的选项卡栏中,点击“选择器统计信息”进行查看。

图片

DevTools 现在提供了浏览器引擎在此重新计算操作期间计算的所有 CSS 选择器的列表,可以按选择器处理时间或匹配次数对选择器进行排序。

如果发现一个选择器需要很长时间来处理,并且匹配了很多次,那么它可能就是一个可以优化的对象。选择器是否可以简化?是否可以更具体地描述需要匹配的元素?

案例分析

下面通过一个照片库案例,看看如何改进 CSS 选择器的性能!

图片

这个页面顶部有一个工具栏,可以按相机型号、光圈、曝光时间等过滤照片。现在在相机型号之间切换感觉有点慢。所以,主要关注如下场景:

  1. 加载页面,并等待过滤器准备就绪;
  2. 将相机型号切换到另一个值并开始记录性能;
  3. 切换回所有相机型号并停止录制。

当切换回所有相机型号时速度很慢,因此只需要测量这一过程。我们还将 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,即::selection前面没有空格。因为这个错误的空格,选择器实际上被引擎解析为 .gallery .photo .meta *::selection,这使得在样式重新计算期间匹配起来要慢得多,因为引擎需要检查所有 DOM 元素,然后验证它们是否嵌套在正确的祖先中。

如果没有多余的空格,引擎只需要检查元素是否具有.meta类,然后再继续即可。

改进 :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] 为前缀,当页面上的文本方向是从右到左时,rtl 方向规则会覆盖左边距并将其替换为右边距。

为了改进这一点,可以使用 CSS 逻辑属性。可以使用符合任何文本方向的逻辑边距方向,而不是指定物理边距方向:

.gallery .photo .meta li strong:empty {
padding: .125rem 2rem;
margin-inline-start: .125rem;
background: var(--dim-bg-color);
}

这样,第二个选择器 html[dir="rtl"] .gallery .photo .meta li button 就可以去掉了。

改进 [class*="gallery-icon--"] 选择器

以下是使用此选择器的 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(...); }

这里可以通过图标类给元素添加对应的图标。这就要求引擎读取类名并对其进行子字符串搜索。可以通过以下方式帮助引擎减少工作量:

.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">,这样就无需再读取类名以及搜索子字符串,减少了浏览器引擎的工作量。

改进 .gallery .photo .meta li 选择器

这个选择器会强制浏览器去检查 li 元素的祖先列表中的多个层级。案例的网页中有很多 li 元素,这可能意味着很多工作。

可以通过为 li 元素指定一个特定的类并删除不必要的嵌套来简化它:

.photo-meta {
display: flex;
align-items: center;
gap: .5rem;
height: 1.5rem;
}

改进 * 选择器

*符号在 CSS 中用作匹配任何元素的通用选择器。这种匹配任何内容的能力意味着引擎需要将相关规则应用于所有元素。在性能记录中可以看到,这个选择器被匹配了很多次。

在案例中,它应用了一个特定的 box-sizing 值:

* {
box-sizing: border-box;
}

这在 CSS 中很常见,但最好删除它,仅在需要时设置 box-sizing 属性。

改进结果

完成以上改进之后,再来查看这个场景的性能情况:

图片

在第一次性能记录中,同样的“重新计算样式”块运行时间几乎为一秒,现在运行时间约为 300 毫秒,性能获得了很大的提升!

总结

案例研究表明,改进某些 CSS 选择器可以带来重要的性能提升。然而,关键是要记住,这主要取决于特定用例。可以使用性能工具来测试网页的性能,如果发现样式重新计算使渲染变慢,就使用 Edge 浏览器中新的“选择器统计信息”功能!

参考:https://blogs.windows.com/msedgedev/2023/01/17/the-truth-about-css-selector-performance/

责任编辑:武晓燕 来源: 前端充电宝
相关推荐

2010-08-26 12:47:15

CSSclass

2010-09-06 09:57:01

CSS类选择器CSS

2010-09-06 09:41:28

CSS继承

2013-03-11 10:30:56

CSSWeb

2010-09-07 11:14:32

CSS属性选择器CSS

2023-03-16 10:20:55

CSS选择器

2010-09-03 09:30:29

CSS选择器

2020-10-25 08:57:56

CSS前端浏览器

2010-09-06 08:52:00

CSS选择器

2022-04-14 09:01:19

CSS父选择器CSS类

2010-12-27 16:01:45

jQuery选择器

2011-10-24 10:30:20

CSS

2022-04-01 09:02:19

CSS选择器HTML

2024-03-27 09:09:40

CSS选择器

2022-12-23 10:26:09

CSShas()

2010-09-06 09:34:16

CSS派生选择器

2010-09-06 09:50:34

id选择器CSS

2010-08-26 12:53:40

CSSid选择器

2010-09-16 15:32:20

CSS选择器

2010-09-07 11:31:23

CSS派生选择器CSS
点赞
收藏

51CTO技术栈公众号