专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端之巅  ·  80人小团队,限时任务!NYPL如何用Nex ... ·  4 天前  
前端大全  ·  太荒谬了!千人公司一刀切禁用 ... ·  6 天前  
前端大全  ·  大家都该学学的埋点概念与使用 ·  6 天前  
前端早读课  ·  【早阅】8 条设计高效 API 的建议 ·  5 天前  
51好读  ›  专栏  ›  前端早读课

【第2849期】探究 CSS 选择器的性能真相

前端早读课  · 公众号  · 前端  · 2023-02-07 07:31

正文

前言

事情一旦做细都不容易。今日前端早读课文章由 @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 中的性能工具可以列出样式重新计算中成本最高的选择器。使用方法如下:

  • 打开 DevTools 中的性能工具;

  • 点击右上角的齿轮图标打开工具设置。

  • 选中 Enable advanced rendering instrumentation (slow) 选项;

  • 点击录制按钮,在要改进的网页上执行特定场景,然后单击停止;

  • 在记录的配置文件中,确定要改进的重新计算样式,并在瀑布视图(“主要” 部分)中选择它;

  • 在底部的选项卡栏中,点击 “选择器统计信息” 进行查看。

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

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

案例分析

下面通过一个照片库案例,看看如何改进 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,即::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(...); }

现在,不只使用一个类,而是向元素添加两个类: