书写高效的CSS选择器

高性能CSS并不是一个新话题,也没有花太多时间研究,但自从入职SKY[作者的工作单位] 以来,我就更加关注CSS设计方便的性能问题。有很多人根本没有意识到CSS选择器的设计即有高效的一面同时也有低效的一面。当你意识到你在css选择器设计过程中,一些细微修改后能够使性能降低,那么以后也就很容易避免这些性能问题了。

这些规范和细节仅仅针对那些性能要求比较高的,需要渲染成千上万DOM节点的网站,话说回来亲自动手测试一下,不论你搭一个 facebook 或者仅仅就是本地的一个页面,仔细测试下都比较容易看出css选择器的设计对页面性能的影响。

CSS选择器

css选择器我们都不陌生,一些基本的分别是标签选择器 (如 div)、id选择器 (如 #header) 、类选择器 (如 .tweet)。不常用的是伪类选择器(如 :hover)、复杂一点的包括css3及正则表达式(如,:first-child or [class^="grid-"]).

选择器的内部有一个性能排序,从高到低的性能排序如下

1. ID, 如 #header
2. 类, 如 .promo
3. 标签, 如 div
4. 相邻, 如 h2 + p
5. 子元素, 如 li > ul
6. 后代, 如 ul a
7. 通配符, 如 *
8. 属性, 如 [type="text"]
9. 伪类, 如 a:hover

理论上尽管id选择器比其他选择器更快一点,但仅此而已,使用 Steve Souders 的测试工具测试后不难发现类选择器的回流时间和ID 选择器是比较接近的。

甚至在 windowsFirefox 6 浏览器测试后发现一个简单的类选择器回流时间为10.9,而ID选择器平均为12.5 ,足以说明id选择器有时比类选择器还要慢。

id选择器和类选择器的速度差异有时几乎没有什么可比性,测试一个标签选择器<a>要比id选择器和类选择器消耗更多的时间,测试一个使用过多限定符的后代选择器,回流时间竟可以在440左右。

由此可以看出[id选择器][类选择器]和[标签选择器][后代选择器]的区别还是很大的,但是他们自身的差异却很小。注意,这些数据受机器和浏览器环境影响较大,建议你自己亲自跑一下数据。

组合选择器设计

你可以单独的使用id选择器如#nav,它会选取任何id为nav的元素,或者你可以使用组合选择器如#nav a,它会匹配那些在id为nav内的元素。

我们从左向右的顺序读,会认为 #nav包含了一个a,但是浏览器不同,浏览器会从右向左读。它会认为是a包裹在#nav里,这一点点细微的读取差别就会出现一个很大的性能问题,并且是值得我们去学些的。如果想知道更深奥的原理,请参考这篇讨论,对浏览器而已从最右边的元素(也就是我们要选择的元素)自下往上遍历dom树要比自上向下遍历效率快(甚至可能无法定位到所选元素) – 下面会提到为什么这么做。

关键字选择器设计

关键字选择器也就是我们最右边我们想要的那个元素,这是浏览器最初查找的元素。
回想一下我们刚提到的哪种选择器最高效?其实,无论哪一个是作为关键字都会影响选择器的性能;当设计高性能的css样式时,正是由关键字决定了准确、关键的性能因素。
关键字选择表示如下:

1
#content .intro{}

把尽可能大的提升性能的指标用类表示是一种固有的性能选择器方案。浏览器将首先查找所有.intro的实例(页面可能没有太多的.intro),然后再去匹配那些id为content并且在包裹在其内部的.intro类元素。相反,下面的选择器书写方式其实很不高效:

1
#content * {}

它所做的就是查找出每一个页面上的元素,然后再去判断它是否在父元素#content内,这是非常耗性能的一种设计模式。通过这些知识点,我们针对类和元素的选取就更容易下结论了。
让我们设想一下,比方你有一个非常非常大的页面,页面中包含成千上万个<a>标签,同时有一些媒体网站链接的<a>标签位于id#social<ul>里面,比方说就是TwitterFacebookDribbbleGoogle+这四个网站链接吧,它们周围还有成千上万个<a>节点都在页面内。因此以下的写法是非常不合理而且低效的:

1
#social a {}

这么做的话,浏览器会访问成千上万个<a>标签,因此我们的关键字选择器匹配了一大堆我们不需要的<a>链接(我们只要那四个匹配媒体网站)造成了极大的性能开销。
为了解决这个问题,我们可以明确地在<a>标签内指定一个叫.social-link类作为关键字,但是跟我们想的不太一样,因为之前我们只知道不要在元素里添加一些无用的类,聊到这里,这就是比较有趣的地方了,其实这是一个奇怪的web标准的最佳实践和纯粹的速度之间的平衡。我们之前会有如下的页面排版:

1
2
3
4
5
6
<ul id="social">
<li><a href="#" class="twitter">Twitter</a></li>
<li><a href="#" class="facebook">Facebook</a></li>
<li><a href="#" class="dribble">Dribbble</a></li>
<li><a href="#" class="gplus">Google+</a></li>
</ul>

和下面的css选择方式

1
#social a {}

但是现在我们修改后的页面排版如下:

1
2
3
4
5
6
<ul id="social">
<li><a href="#" class="social-link twitter">Twitter</a></li>
<li><a href="#" class="social-link facebook">Facebook</a></li>
<li><a href="#" class="social-link dribble">Dribbble</a></li>
<li><a href="#" class="social-link gplus">Google+</a></li>
</ul>

和下面修改后的css选择方式

1
#social .social-link {}

这个新的关键字选择器将会查找和匹配更少量的元素,意味着浏览器可以更快的渲染它们以腾出时间来做后面的工作。其实我们可以直接用.socaial-link表示,并不需要在前面加上#socail限定符;接下来的部分会讲到……

所以简单的讲,关键字选择器决定了浏览器要做多少工作,这就是我们值得注意的地方。

限定符嵌套选择器设计

讲到这里,我们知道了什么是关键字选择器,这也是写代码需要多考虑的地方,但是我们还能做更深层次的进行优化。一个设计良好的关键字选择器是会尽量避免太多的限定符嵌套的。多限定符如下所表示:

1
#content a {}

会发生什么呢?
首先浏览器第一件事就是在页面上搜索所有的<a>标签,完了之后还会检查哪些<a>标签在id为#content内,一直找一直找,直到找到最顶层的html元素,这也就导致浏览器做了太多不必要的检查工作,我们可以举一个更现实的例子如下:

1
#nav li a {}

直接降级为如下形式:

1
#nav a {}

我们知道ali内,它肯定也就在#nav内,所以我们可以直接把li从选择器里移除,又因为idnav的元素在页面只存在一个,所以在此范围内其他元素是完全不相关的,因此我们也可以移除类似ul的元素。

总结,所限定符选择器方式会增加浏览器工作的负担;通过删减一些没必要的元素,尽可能地把你的选择器设计的更简洁、更高效。

真的要这样做吗?

简短的回答是:可能不必!
完整的回答是:根据你搭建的网站的情况,如果你在开发下一代portfolio,就会更关注与代码的整洁而不是css的性能,因为你真的不可能注意到的。但是如果你在维护亚马逊
这样的网站,网站页面往往毫秒间就会有不同结果,那么你可能注意一下,但也可能你仍旧不会注意。

浏览器会更好的处理css的解析速度;甚至是移动设备。你可能就不曾注意到网页中css选择器自身的性能问题,但是。。。

但是

无论浏览器多快都会做这些工作,css的性能问题也仍旧会存在。即使你不需要,甚至不想理会文中的提到的任意一点,但这些知识都是值得你去了解的,请记住css选择器是非常耗性能的,应该避免冗长的嵌套,如果你发现自己写成类似如下的的语法:

1
div:nth-of-type(3) ul:last-child li:nth-of-type(odd) * { font-weight:bold }

那么你可能就已经出错了!

原文链接 Writing efficient CSS selectors

================================================================
post by 午夜圣斗士 QQ | 邮箱 | Github

目录

  1. 1. CSS选择器
  2. 2. 组合选择器设计
  3. 3. 关键字选择器设计
  4. 4. 限定符嵌套选择器设计
  5. 5. 真的要这样做吗?
  6. 6. 但是
    1. 6.0.0.0.1. 原文链接 Writing efficient CSS selectors