本节介绍几个与CSS性能增强相关的属性。
13.7.1 慎用will-change属性提高动画性能
will-change
属性的作用很单纯,就是“增强页面渲染性能”,那它是如何增强的呢?
在现代浏览器中,3D 变换会启用 GPU 加速。例如,应用 translate3D()
、scaleZ()
之类的变换函数会启用 GPU 加速。但是,这些 CSS 语句在我看来属于“hack 性能加速法”,因为实际上大多数的动画不需要 z轴的变化,但 CSS 还是假模假样地声明了,欺骗浏览器并不是一种值得说道的做法。而 will-change
属性则天生为性能加速而设计,顾名思义——“我要变化了”,礼貌而友好。
当我们通过某些行为(点击、移动或滚动)触发页面进行大面积绘制的时候,浏览器往往是没有准备的,只能被动使用 CPU 去计算与重绘。由于没有事先准备,因此会产生卡顿现象,而will-change
属性会在真正的行为触发之前告诉浏览器:“我待会儿就要变化了,你要做好准备。”于是,浏览器准备好GPU,从容应对即将到来的内容或形状的变化。
1.will-change属性的语法
will-change
属性的语法如下:
使用示意如下:
下面讲一下各个属性值都应该在什么状况下使用。如果发现滚动动画卡顿,则可以试试scroll-position
;如果是内容变化,则可以试试contents
;如果是其他CSS属性动画性能不佳,掉帧明显,则可以试试<custom-ident>
类型的属性值。
虽然<custom-ident>
数据类型指的是任意自定义的名称,但是只有CSS属性对应的名称才有效果。也就是说,虽然下面的语法都是正确的,却没有任何效果:
因此,在实际的操作中,will-change
属性的属性值都是合法的CSS属性。
再说一个有趣的现象,transform
和opacity
的动画性能是最高的,其他CSS属性,如margin
、padding
、border-width
和background-position
等,都是动画性能低下的CSS属性,因此,will-change
属性的属性值应该是margin
和padding
之类的CSS属性。但是实际上,我们平常开发看到更多的will-change
属性值是transform
和opacity
属性。
为什么呢?原因就在于使用的方向“歪”了。
现代浏览器不是过去的IE浏览器,性能都是不错的,就算margin
和padding
等CSS属性的性能低下,也只是相对而言的,用在日常开发中是不会出现用户可以感知的动画卡顿的,也就是说,虽然will-change
属性的设计初衷是提高动画性能,但是需要使用will-change
属性提高动画性能的需求并不多见,因此很少见到下面的CSS代码:
也就是说,使用will-change
属性提高动画性能的需求并非普遍需求,而will-change: transform
或will-change:opacity
属性并不是用来提高动画性能的,而是为了创建新的层叠上下文,或者是为了解决iOS的Safari浏览器中的一些奇怪的渲染问题。
will-change
属性有一个隐藏的特性,那就是使用某个CSS属性作为属性值之后,元素会有与当前CSS属性类似的行为。例如,元素设置will-change:transform
后会有与元素设置transform
属性(属性值不是none
)一样的行为,包括:
- 会创建新的层叠上下文,影响元素的层级;
- 会影响混合模式的渲染计算;
- 设置
overflow:hidden
会隐藏内部溢出的绝对定位元素。
因此,will-change:transform
经常出现,不是为了提升动画性能,而是为了背后的渲染特性,它可以解决iOS Safari浏览器中一些奇怪的渲染问题。will-change:opacity
经常出现的原因也是类似的。
最后,如果will-change
属性的属性值是一个CSS缩写属性,如设置will-change: background
声明,则所有与background
缩写相关的属性发生变化的时候都会触发性能加速。
2.慎用will-change属性
既然will-change
属性可以提高渲染性能,我们是否可以给所有动画元素都设置will-change
属性呢?
千万不要这么做,will-change
属性提高渲染性能是有成本的。这个成本就是GPU内存,在移动端设备上,更直观的反映是手机会发烫,电量消耗会特别快。
日常的动画效果都是非常流畅的,根本没有必要使用will-change
属性来加速。同时,就算使用will-change
属性也要遵循最小化影响原则,例如不要设置will-change
属性在默认状态中,否则“GPU层”会一直存在,GPU开销也会一直存在,一旦匹配will-change
属性的元素较多,性能开销就很大了。因此,避免使用下面的CSS代码:
更推荐的做法是在父元素的:hover
伪类状态中声明will-change
属性,这样鼠标指针移出当前元素的时候会自动清除GPU开销,代码如下:
注意,不能在当前元素的:hover
伪类中设置will-change
属性,也就是不能使用下面的写法:
因此will-change
属性需要预声明才有意义。悬停效果几乎总是先由父元素触发,然后才到子元素,因此will-change
属性需要在父元素的:hover
伪类状态中设置。
如果使用JavaScript代码添加will-change
属性,则事件结束或动画完毕的时候一定要==及时清除will-change
属性==。
例如,点击某个按钮,然后某个元素会执行动画效果。当用户点击一个按钮时,先执行的是mousedown
事件,紧接着才是click
事件,因此,我们可以在执行mousedown
事件的时候添加will-change
属性,动画结束的时候再使用动画效果自带的回调函数移除will-change
属性,代码如下所示(target
表示目标动画元素):
兼容性
所有现代浏览器均支持will-change
属性,完整的兼容性如表13-12所示。
表13-12 will-change
属性的兼容性(数据源自Caniuse网站)
IE | Edge | Firefox | Chrome | Safari | iOS Safari | Android Browser |
---|---|---|---|---|---|---|
✘ | ✘ | 36+ ✔ | 36+ ✔ | 9.1+ ✔ | 9.3+ ✔ | 5+ ✔ |
13.7.2 深入了解contain属性
contain
属性是CSS Containment模块规范中定义的CSS属性,作用是提高Web页面的渲染性能。
Web网页的HTML本质上是一个DOM树,在默认情况下,某一个节点的样式变化会触发整个文档树的重绘和重计算,这是DOM渲染最大的性能开销。
contain
属性可以让局部的DOM树结构成为一个独立的部分,和页面其他的DOM树结构完全隔离,这样在这部分内容发生变化的时候,重绘与重计算只会在这个局部DOM树结构内部发生。于是,性能就会有非常显著的提升。contain
属性非常适合用在复杂页面的某个小组件上,可以有效避免“牵一发而动全身”的情况出现。
contain
属性理论上应该是一个让人足够兴奋的属性,但是由于浏览器本身的渲染性能越来越好,因此,目前在日常开发中,绝大多数页面是完全不需要考虑所谓的渲染性能问题的。即使有上万个DOM节点,Chrome浏览器也能在很短的时间内渲染出来。
在过去,contain
属性可以提升超过 100 倍的渲染性能,但是同样的测试页面,使用contain
属性和不使用contain
属性看起来并没有多大的区别。这样就出现了一个很尴尬的情况,浏览器自身足够优秀,导致contain
属性并没有多少机会绽放光彩。
根据我自己的相关测试,在有些场景下,使用 contain
属性还是会有性能上的提升的。读者可以在浏览器中进入 https://demo.cssworld.cn/new/13/7-1.php 页面,或者扫描右侧的二维码查看这个测试页面。
1000层标签嵌套,最内部元素被点击时内容会发生变化。在Firefox浏览器中,设置了contain: strict
的元素在被点击的时候几乎没有渲染时间的开销,如图13-22所示。
图13-22 设置contain:strict
后的渲染时间示意
因此,contain
属性还是有一定的实用价值的。
1.CSS Containment中的一些概念
想要彻底了解contain
属性,需要对CSS Containment中的限制类型有所了解,限制类型如下:
- Size Containment;
- Layout Containment;
- Style Containment;
- Paint Containment。
不同的限制类型对应不同的渲染限制,了解它们有助于更精准地进行性能提升控制。
(1)Size Containment。
Size Containment可以被近似地理解为“尺寸限制”,为什么说近似呢?这是因为我们日常所说的“尺寸限制”指的是max-width/min-width
这种最大/最小尺寸限制,但是这里的“尺寸限制”指的是内部元素的变化不会影响当前元素尺寸的变化。
举个简单的例子,有一个图片元素,其HTML代码如下:
此时,这张图片就有一个尺寸,这个尺寸在默认状态下是1.jpg这张图片的原始尺寸。现在,我们将1.jpg修改为另外一个不同尺寸的2.jpg,则<img>
元素的尺寸就会跟着变化。但是,如果给这个图片元素应用Size Containment,则无论src
属性链接的图片尺寸有多大,<img>
元素的尺寸都不会发生变化,这就是“尺寸限制”的含义。
下面问题来了,“尺寸限制”的行为是如何实现的呢?很简单,让浏览器直接无视元素里面的内容就可以了,也就是假设元素里面的元素不存在;如果是替换元素,就认为替换内容不存在。因此,Size Containment状态下的元素的content-box
尺寸都是0×0,如果没有设置边框等样式,这个元素就是不可见的。因此,实际开发的时候,应用Size Containment的元素一定是需要设置具体的width
和height
属性的,也就是必须设置具体的尺寸值。
不是所有元素都支持Size Containment的,不支持Size Containment的元素包括设置了display:contents
和display:none
的元素,内部display
类型是table
的元素,<td>
、<th>
和<tr>
这些内部表格元素,常规的内联元素等。
实际上,在CSS渲染的性能优化中,Size Containment出场的机会并不多,按照CSS规范文档中的说法,特别适合使用Size Containment的场景是使用JavaScript根据包含块元素的尺寸设置内部元素尺寸,这样可以有效避免某种“无限循环”。
举个例子,inline-block
水平的元素的尺寸是根据元素里面的内容决定的,现在希望元素里面内容的尺寸永远比inline-block
的尺寸小1px。按照字面上的需求,我们可以先使用JavaScript获取inline-block
水平的尺寸,再去修改元素里面子元素的尺寸。但是,这种做法会带来一个问题,那就是inline-block
的尺寸是根据元素里面子元素的尺寸变化的,如果子元素尺寸变小了,岂不是inline-block
元素的尺寸也要变小,这又会导致子元素尺寸再次变小……一个循环的过程就产生了。
使用Size Containment可以有效避免这样的渲染情况出现,因为inline-block
元素的尺寸摆脱了对内部元素内容尺寸的依赖。
(2)Layout Containment。
Layout Containment指的是“布局限制”,可以想象成对元素的骨架、框架或者渲染盒子进行了封闭,形成了一个真正意义上的“结界”。这些限制会给元素带来很多和普通元素不一样的特性。
- 会形成一个全新的包含块,无论是绝对定位元素还是固定定位元素的
left
和top
偏移都会相对于这个包含块元素计算。transform
属性也有这个特性,在transform
属性值不是none
的元素中的固定定位元素的样式表现如同绝对定位。contain:layout
也可以让设置了position:fixed
的固定定位元素像绝对定位元素那样表现,即left
和top
值是相对于设置了contain:layout
的元素偏移的,而不是相对于浏览器窗体的,并且元素可以滚动,而不是位置固定。 - 会创建一个全新的层叠上下文,除了可以改变元素重叠时的层级表现,还可以限制混合模式等CSS特性的渲染范围。
- 会创建一个新的块状格式化上下文(Block Formatting Context,BFC),因此,Layout Containment状态下的元素是不会受到浮动元素干扰的。
Layout Containment还有下面这些大家可以不用在意的特性。
- 如果
overflow
的属性值是visible
、clip
或两者的组合,则元素内任意元素内容的溢出都不会影响外部元素的布局。 - 内部的元素可以分栏、分区,但是不能传播到父元素。
- 基线消失,或者可以认为底边缘是基线。
和Size Containment类似,Layout Containment同样对隐藏元素、表格元素(不包括table-cell
元素)和纯内联元素无效。
(3)Style Containment。
Style Containment和很多人预想的不一样,并不是指常规的样式限制,而是指CSS计数器和其他相关内容生成的限制。例如,CSS计数器属性counter-increment
和counter-set
是受到整个DOM树中的计数器影响的。
例如,父元素执行一次counter-increment
,子元素又执行一次counter-incre ment
,则最终的计数值是父、子元素的累加值。但是,如果设置了Style Containment,则计数范围就会被限定在元素的子树上,而不是整个树。
举个例子,HTML和CSS代码如下:
在这个例子中,counter-reset
属性被限制在了<div>
元素的子树中,外部元素设置的同名counter-reset
则会被忽视,因此,最终并不会出现序号级联的效果。但是,如果删除contain:style
这段CSS样式,则计数器的范围限制就没有了,于是就有了序号级联的效果。最终的对比效果如图13-23所示。
图13-23 contain:style
对计数器范围的限制示意
眼见为实,读者可以在浏览器中进入 https://demo.cssworld.cn/new/13/7-2.php 页面,或者扫描右侧的二维码查看效果。
Style Containment除了限制计数器的作用范围,对其他content
内容生成特性同样适用,包括open-quote
、close-quote
、no-open-quote
和no- close-quote
。
content
内容生成虽然很实用,但是很遗憾,目前除了基本的数字序号生成,平常很少见到content
属性的其他高级应用,因此,Style Containment也被拖累成一个出场机会很少的限制特性。目前Firefox浏览器并不支持Style Containment,因此我不看好Style Containment的未来。
(4)Paint Containment。
就表现而言,Paint Containment和Layout Containment有不少相似之处,都会成为绝对定位和固定定位元素的包含块,会创建新的层叠上下文和格式化上下文。
当然,它们的不同之处也很明显,那就是Paint Containment不会渲染任何包含框以外的内容,哪怕overflow
属性值是visible
;并且Paint Containment依然会保留溢出内容对布局的影响(例如会改变元素的基线位置)。请看下面这个例子:
此时,按钮就会和<p>
元素保持基线对齐,而此时<p>
元素的基线就是内部文本的基线,于是会有图13-24所示的效果。
图13-24 按钮和大段文字默认的基线对齐表现示意
接下来继续给<p>
元素设置contain:paint
,CSS代码如下:
结果溢出容器的文字内容直接不渲染了,表现为透明不可见,但是大家可以发现,<p>
元素的基线位置依旧和之前一样,效果如图13-25所示。
图13-25 contain:paint
不渲染容器外的文字内容示意
这就是contain:paint
和overflow:hidden
隐藏容器外元素重要的区别之一。
2.contain属性的语法
明白了CSS Containment的4种限制类型,学习contain
属性的语法就轻松多了。
contain
属性的语法如下:
先讲一下这一语法中的几个关键点。
- Size Containment限制类型对应属性值
size
,不妨就称为size
类型。 - Layout Containment限制类型对应属性值
layout
,不妨就称为layout
类型。 - Style Containment限制类型对应属性值
style
,不妨就称为style
类型。 - Paint Containment限制类型对应属性值
paint
,不妨就称为paint
类型。
此时,strict
和content
这两个属性值的含义就一目了然了。
strict
表示对除style
类型以外的类型都进行限制。此属性值等同于contain: size layout paint
的设置。content
表示对除size
和style
类型以外的类型都进行限制。此属性值等同于contain: layout paint
,表现为元素内内容渲染,元素外内容不渲染。
大家可以根据合适的场景选择合适的contain
属性值来优化CSS渲染的性能。
兼容性
contain
属性本身是一个增强体验的CSS属性,因此无论兼容性如何,我们都可以放心大胆地在实际项目中使用。
目前Safari浏览器还没有支持contain
属性,Chrome和Firefox浏览器均支持contain
属性(准确地讲,Firefox是绝大部分支持)。该属性完整的兼容性信息如表13-13所示。
表13-13 contain
属性的兼容性(数据源自Caniuse网站)
IE | Edge | Firefox | Chrome | Safari | iOS Safari | Android Browser |
---|---|---|---|---|---|---|
✘ | ✘ | 69+ ✔ | 52+ ✔ | ✘ | ✘ | 5+ ✔ |
13.7.3 content-visibility属性
content-visibility
属性可以让浏览器决定是否渲染视区以外的元素的内容,借此提高页面的渲染性能。
根据目前我查阅到的某个案例的数据,渲染时间是232 ms的网页使用content-visibility
属性优化后可以降低到30 ms。看起来这个属性很强大,但是我保持“谨慎的乐观”,因为如果content- visibility
属性真的这么好,浏览器应该会默认支持,所以很明显,content-visibility
属性带来的性能提升一定是牺牲了某些东西才实现的,例如快速滚动页面时的加载体验。
当浏览器决定不渲染某个元素里面的内容的时候,元素会开启Layout Containment、Style Containment和Paint Containment,如果元素没有设置具体的高、宽值,则尺寸可能是0。随着浏览器页面的滚动,元素进入视区后会再次渲染,此时就会出现内容跳动的情况,这种体验反而糟糕。
目前Chrome 85+浏览器已经支持了content-visibility
属性,大家如果对这个属性感兴趣,可以试试使用类似下面的CSS代码给自己的项目做一个测试,看看效果如何:
其中contain-intrinsic-size
属性可以理解为内容的占位尺寸。
由于content-visibility
属性目前尚未成熟,因此暂时就说这么多。
[1] GPU即图形处理器,是与处理和绘制图形相关的硬件。GPU是专为执行复杂的数学和几何计算而设计的,可以让CPU从图形处理的任务中解放出来,从而执行其他更多的系统任务,如页面的计算与重绘。