大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 The truth about CSS selector performance。
本期共享的是,如果您是一名 Web 开发者,您可能听说过某些 CSS 选择器比其他选择器性能更快。果真如此吗?
B 站方言,是,但不完全是。
幕后花絮速览
编写 CSS 选择器的方式确实会影响浏览器渲染网页的方式。
每当页面局部变化时,运行它的浏览器引擎就需要查看新的 DOM 树,并根据可用的 CSS 样式表确定如何设置其样式。这种将样式与 DOM 节点匹配的操作称为样式重新计算(style recalculation)。
在不涉及一大坨细节的情况下,浏览器引擎需要查看所有规则,并决定哪些规则适用于给定元素。为此,引擎需要查看规则选择器,并且从右到左进行。
举个栗子,当引擎瞄到诸如 .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 树,大概率会导致性能不佳。但存在一个平衡点。仅仅为了取悦 linter,并期望获好的性能,而对理论规则过度索引和更改选择器可能只会使我们的 CSS 更难以阅读和维护,而没有多少实际收益。
因此,请以对 App 有意义、且易于阅读和维护的方式编写代码,然后测量重要用户场景的实际性能。
测量
更应该测量我们的关键 App 场景,而不是盲目地应用一组如何编写快速代码的规则。了解我们可用的工具并使用它们。
微软 Edge Devtool(开发工具)有一个 Performance(性能)工具,当我们的 App 开始"感觉"卡顿时,它可以真正让您大开眼界。
粉丝请注意"感觉"这个措辞。请为我们的用户建立同理心,并尽可能使用它们实际使用的设备。我们的开发机器可能比用户的设备更给力。
事实上,使用 DevTools 可以做的一件好事就是,直接从工具内减慢 CPU 和网络连接速度。
性能工具可能看起来相当复杂,但我们有文档辅助。此外,一切都只发生在我们的浏览器中,因此我们可以在不破坏任何内容的情况下尝试,并且如果遭遇麻烦,我们始终可以重载页面,并重开 DevTools。
学习使用可用的工具来衡量我们的关键场景,并学习识别导致速度变慢的"万恶之源"。
如果样式重新计算确实是导致 App 变慢的原因之一,那么我们有个好消息给您。当涉及到调查我们聚焦的性能问题时,没有什么比拥有一个能够立即为我们提供问题根本原因的工具更好的了。
选择器统计数据来拯救开发
从微软 Edge 109 开始,DevTools 中的性能工具可以列出任何样式重新计算中成本最高的选择器。获取方法如下:
- 打开Performance工具。
- 单击右上角的齿轮图标打开该工具的设置。
- 选中启用**高级渲染检测(缓速)**选项。
- 单击录制 ,在要优化的网页上执行场景,然后单击停止。
- 在记录的配置文件中,确定要优化的长样式重新计算,并在瀑布视图的主要部分选中它。
- 在底部选项卡栏中,单击选择器统计信息。
DevTools 现在为我们提供浏览器引擎在此重新计算操作期间计算的所有 CSS 选择器的列表。我们可以按选择器处理时间或匹配次数对选择器排序。
如果我们发现某个选择器需要很长时间处理,并且匹配了很多次,那么它可能是一个值得尝试优化的最佳备胎。比如选择器可以简化吗?是否可以使其更具体地描述其应匹配的元素?
此新功能可以立即从可疑的样式重新计算转到导致其如此长的各个 CSS 选择器。然后,我们可以返回源码,优化这些特定的选择器,然后再次测量。
案例分析
为了知行合一,让我们尝试优化一个实际的网页。我们将使用为此目的构建的照片库页面作为演示。
该页面顶部有一个工具栏,可以按相机型号、光圈、曝光时间等过滤照片,并且现在在相机型号之间切换感觉有点卡顿。
尽管此演示页面专门为此构建,但它确实展示了一个与我们在微软自家产品中遭遇的情况类似的案例。Edge 团队和微软依赖 Web 平台的其他产品团队在这一领域"梦幻联动",创造最佳的用户体验。在某些特定场景中,我们在具有大量 DOM 元素的 App 中发现异常长的样式重新计算,比如我们会在此处使用的演示页面,其中包含大约 5000
个元素。访问 CSS 选择器统计工具对我们帮助很大。
我们会关注的场景如下:
- 加载演示页面,并等待过滤器准备就绪。
- 将相机型号过滤器切换到另一个值,并开始记录性能。
- 切换回所有相机型号,并停止录制。
切换回所有照片的速度很慢,因此我们只测量这部分。我们还将 CPU 速度降低 4 倍,谋求比在强大的开发机器上获得的更真实的结果。
一旦记录准备就绪,我们可以轻而易举地在配置文件中看到一个很长的重新计算块,在我们例子中总计超过 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
,设置页面照片元数据部分中用户选择的背景和文本颜色的样式。当用户选择照片下方的文本时,会使用自定义颜色,而不是浏览器的默认颜色。
由于代码中的错误,这种特殊情况实际上有 bug。选择器实际上应该是 .gallery .photo .meta::selection
,而 .meta
和 ::selection
之间没有多余的空格。
因为那里有一个额外的空间,我们的选择器实际上被引擎解释为:.gallery .photo .meta *::selection
,这使得在样式重新计算期间匹配速度变慢,因为引擎需要检查所有 DOM 元素,然后验证它们是否嵌套在正确的祖先元素中。
如果没有额外的空间,引擎只需要在进一步检查之前,检查元素是否具有 .meta
类。
优化 :empty
选择器
选择器 .gallery.photo .meta li Strong:empty
乍一看十分可疑。:empty
伪表示选择器当且仅当 strong
元素没有任何内容时才匹配。
这可能需要引擎做更多的工作,而不仅仅是检查元素的标签名称,但这非常有用。
虽然但是,看看与此规则接近的其他 CSS 规则,我们可以看到以下内容:
css
.gallery .photo .meta li strong:empty {
padding: 0.125rem 2rem;
margin-left: 0.125rem;
background: var(--dim-bg-color);
}
html[dir='rtl'] .gallery .photo .meta li strong:empty {
margin-left: unset;
margin-right: 0.125rem;
}
相同的选择器重复两次,但第二个实例以 html[dir=rtl]
为前缀,当页面上的文本方向从右到左时,这对于覆盖第一个规则很有用。在此情况下,rtl
方向规则会覆盖左边距,并用右边距代替。
为了改善这点,我们可以使用 CSS 逻辑属性。我们可以使用适应任何文本方向的逻辑方向,而不是指定物理边距方向,如下所示:
css
.gallery .photo .meta li strong:empty {
padding: 0.125rem 2rem;
margin-inline-start: 0.125rem;
background: var(--dim-bg-color);
}
当我们这样做时,CSS 代码中的其他使用同款属性选择器的地方,也可以使用逻辑 CSS 属性优化。举个栗子,我们可以去掉之前找到的 html[dir="rtl"] .gallery .photo .meta li button
选择器。
优化 [class*=" gallery-icon--"]
选择器
我们的下一个选择器是这个有点复杂的属性选择器:[class*=" gallery-icon--"]::before
。
属性选择器非常有用,因此在删除它们之前,请检查它们是否真的会产生负面影响。在我们的例子中,这个选择器似乎确实物尽其用。
以下是我们使用此选择器的 CSS 规则:
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(...);
}
这里的想法是,我们可以将这些图标类中的任何一个分配给一个元素,它将获得相应的图标。
虽然这是一个便捷功能,但我们要求引擎读取类值,并对其进行子字符串搜索。这是我们可以辅助引擎减少工作的一种方法:
css
.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
选择器
该选择器看起来确实没什么攻击性。但是,如上所述,它仍然强制浏览器去检查 li
元素的祖先列表中的多个层级。如果我们的网页有一大坨 li
元素,这可能需要大量工作。
我们可以通过为 li
元素指定一个特定的类,并删除多余的嵌套,简化这一过程。举个栗子:
css
.photo-meta {
display: flex;
align-items: center;
gap: 0.5rem;
height: 1.5rem;
}
优化 *
选择器
*
符号在 CSS 中用作匹配任何元素的通用选择器。这种匹配任何内容的能力意味着,引擎需要将关联规则应用于所有元素。
正如我们在性能记录中看到的,该选择器确实被匹配了很多次。CSS 规则的实际作用值得研究一下。
在我们的例子中,它应用了特定的 box-sizing
值:
css
* {
box-sizing: border-box;
}
这在 CSS 中很常见,但在我们的例子中,删除它实际上是有意义的,按需应用 box-sizing
,然后查看收益。
结果
完成所有优化后,是时候再次检查我们的场景性能了。
在上面的性能记录中,相同的重新计算样式块运行时间几乎为一秒,现在运行时间约为 300
毫秒,这真是一个巨大的成果!
结论
优化某些 CSS 选择器可以带来重要的性能提升。虽然但是,粉丝请注意,这取决于您的特定用例。使用性能工具测试网页的性能,如果我们发现样式重新计算导致场景变慢,请使用 Edge 中的新选择器统计窗口。
本期话题是 ------ 您的 CSS 代码会强制遵循社区的 BEM 等风格指南吗?
欢迎在本文下方群聊自由言论,文明共享。谢谢大家的点赞,掰掰~
《前端 9 点半》每日更新,坚持阅读,自律打卡,每天一次,进步一点。