CSS的匹配机制:从右向左
这一策略导致了不同种类的选择器之间的性能也存在差异。 例如.grandfather .father .son 与 .father-son, 很显然 前者需要匹配两次 而后者只需要匹配一次,相比来说花费的时间也更少。 因为前者需要找到DOM中所有的son 再过滤掉祖先不是father的son元素, 然后再滤掉 father 的祖先不是 grandfather 的son元素。 在一些复杂的场景下,匹配所需要花费的代价自然随着选择器嵌套级别的层数递增
不过现代浏览器在这一方面做了很多优化,不同选择器的性能差别并不明显,甚至可以说差别甚微。此外不同选择器在不同浏览器中的性能表现也不完全统一,在编写CSS的时候无法兼顾每种浏览器。鉴于这两点原因,我们在使用选择器时,只需要记住以下几点,其他的可以全凭喜好。
1、有选择的使用选择器
- 保持简单,不要使用嵌套过多过于复杂的选择器。
- 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。
- 不要使用类选择器和ID选择器修饰元素标签,如
div .grandfather-father
,这样多此一举,还会降低效率。 - 不要为了追求速度而放弃可读性与可维护性。
2、减少使用昂贵的属性
在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写CSS时,我们应该尽量减少使用昂贵属性,如box-shadow
/border-radius
/filter
/透明度/:nth-child
等。
3、优化重排与重绘
在网站的使用过程中,某些操作会导致样式的改变,这时浏览器需要检测这些改变并重新渲染,其中有些操作所耗费的性能更多。我们都知道,当FPS为60时,用户使用网站时才会感到流畅。这也就是说,我们需要在16.67ms内完成每次渲染相关的所有操作,所以我们要尽量减少耗费更多的操作。
-
减少重排
重排会导致浏览器重新计算整个文档,重新构建渲染树,这一过程会降低浏览器的渲染速度。如下所示,有很多操作会触发重排,我们应该避免频繁触发这些操作。
- 改变
font-size
和font-family
- 改变元素的内外边距
- 通过JS改变CSS类
- 通过JS获取DOM元素的位置相关属性(如width/height/left等)
- CSS伪类激活
- 滚动滚动条或者改变窗口大小
-
避免不必要的重绘
当元素的外观(如color,background,visibility等属性)发生改变时,会触发重绘。在网站的使用过程中,重绘是无法避免的。不过,浏览器对此做了优化,它会将多次的重排、重绘操作合并为一次执行。不过我们仍需要避免不必要的重绘,如页面滚动时触发的hover事件,可以在滚动的时候禁用hover事件,这样页面在滚动时会更加流畅。
最后需要注意的是,用户的设备可能并没有想象中的那么好,至少不会有我们的开发机器那么好。我们可以借助Chrome的开发者工具进行CPU降速,然后再进行相关的测试,降速方法如下图所示。
如果需要在移动端访问的,最好将速度限制更低,因为移动端的性能往往更差。
4、CSS - 浏览器内部执行过程
现代的浏览器通常会有两个重要的执行线程,这2个线程协同工作来渲染一个网页:
- 主线程
- 合成线程
一般情况下,主线程负责:
- 运行JavaScript。
- 计算HTML 元素的 CSS 样式。
- 页面的布局
- 将元素绘制到一个或多个位图中
- 将这些位图交给合成线程
相应地,合成线程负责:
- 通过 GPU将位图绘制到屏幕上
- 通知主线程更新页面中可见或即将变成可见的部分的位图
- 计算出页面中哪部分是可见的
- 计算出当你在滚动页面时哪部分是即将变成可见的
- 当你滚动页面时将相应位置的元素移动到可视区域
长时间执行 JavaScript 或渲染一个很大的元素会阻塞主线程,在这期间,它将无法响应用户的交互。
相反,合成线程则会尽量去响应用户的交互。当一个页面发生变化时,合成线程会以每秒60 帧的间隔去不断重绘这个页面,即使这个页面不完整。
举个例子,当用户滚动页面时,合成线程会通知主线程更新页面中最新可见部分的位图。但是,如果主线程响应地不够快,合成线程不会保持等待,而是马上绘制已经生成的位图,还没准备好的部分用白色进行填充。