本节介绍几个与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属性的语法如下:

will-change: auto;
will-change: scroll-position | contents | <custom-ident>

使用示意如下:

will-change: auto;
will-change: scroll-position;
will-change: contents;
/* 下面是<custom-ident>的示例 */
will-change: transform;        
will-change: opacity; 
will-change: left, top;

下面讲一下各个属性值都应该在什么状况下使用。如果发现滚动动画卡顿,则可以试试scroll-position;如果是内容变化,则可以试试contents;如果是其他CSS属性动画性能不佳,掉帧明显,则可以试试<custom-ident>类型的属性值。

虽然<custom-ident>数据类型指的是任意自定义的名称,但是只有CSS属性对应的名称才有效果。也就是说,虽然下面的语法都是正确的,却没有任何效果

will-change: aaa;
will-change: bbb;
will-change: ccc;

因此,在实际的操作中,will-change属性的属性值都是合法的CSS属性。

再说一个有趣的现象,transformopacity的动画性能是最高的,其他CSS属性,如marginpaddingborder-widthbackground-position等,都是动画性能低下的CSS属性,因此,will-change属性的属性值应该是marginpadding之类的CSS属性。但是实际上,我们平常开发看到更多的will-change属性值是transformopacity属性。

为什么呢?原因就在于使用的方向“歪”了。

现代浏览器不是过去的IE浏览器,性能都是不错的,就算marginpadding等CSS属性的性能低下,也只是相对而言的,用在日常开发中是不会出现用户可以感知的动画卡顿的,也就是说,虽然will-change属性的设计初衷是提高动画性能,但是需要使用will-change属性提高动画性能的需求并不多见,因此很少见到下面的CSS代码:

will-change: margin, padding;

也就是说,使用will-change属性提高动画性能的需求并非普遍需求,而will-change: transformwill-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代码:

.will-change {
    will-change: transform;
    transition: transform 0.3s;
}
.will-change:hover {
    transform: scale(1.5);
}

更推荐的做法是在父元素的:hover伪类状态中声明will-change属性,这样鼠标指针移出当前元素的时候会自动清除GPU开销,代码如下:

.will-change-parent:hover .will-change {
    will-change: transform;
}
.will-change {
    transition: transform 0.3s;
}
.will-change:hover {
    transform: scale(1.5);
}

注意,不能在当前元素的:hover伪类中设置will-change属性,也就是不能使用下面的写法:

.will-change {
    transition: transform 0.3s;
}
.will-change:hover {
    will-change: transform;
    transform: scale(1.5);
}

因此will-change属性需要预声明才有意义。悬停效果几乎总是先由父元素触发,然后才到子元素,因此will-change属性需要在父元素的:hover伪类状态中设置。

如果使用JavaScript代码添加will-change属性,则事件结束或动画完毕的时候一定要==及时清除will-change属性==。

例如,点击某个按钮,然后某个元素会执行动画效果。当用户点击一个按钮时,先执行的是mousedown事件,紧接着才是click事件,因此,我们可以在执行mousedown事件的时候添加will-change属性,动画结束的时候再使用动画效果自带的回调函数移除will-change属性,代码如下所示(target表示目标动画元素):

dom.onmousedown = function() {
    target.style.willChange = 'transform';
};
dom.onclick = function() {
// target元素执行动画
};
target.onanimationend = function() {
// 动画结束,用回调函数移除will-change属性
    this.style.willChange = 'auto';
};

兼容性

所有现代浏览器均支持will-change属性,完整的兼容性如表13-12所示。

表13-12 will-change属性的兼容性(数据源自Caniuse网站)

Internet Explorer logo
IE
Edge logo
Edge
Firefox logo
Firefox
Chrome logo
Chrome
Safari logo
Safari
Safari logo
iOS Safari
Android Browser logo
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代码如下:

<img src="1.jpg">

此时,这张图片就有一个尺寸,这个尺寸在默认状态下是1.jpg这张图片的原始尺寸。现在,我们将1.jpg修改为另外一个不同尺寸的2.jpg,则<img>元素的尺寸就会跟着变化。但是,如果给这个图片元素应用Size Containment,则无论src属性链接的图片尺寸有多大,<img>元素的尺寸都不会发生变化,这就是“尺寸限制”的含义。

下面问题来了,“尺寸限制”的行为是如何实现的呢?很简单,让浏览器直接无视元素里面的内容就可以了,也就是假设元素里面的元素不存在;如果是替换元素,就认为替换内容不存在。因此,Size Containment状态下的元素的content-box尺寸都是0×0,如果没有设置边框等样式,这个元素就是不可见的。因此,实际开发的时候,应用Size Containment的元素一定是需要设置具体的widthheight属性的,也就是必须设置具体的尺寸值。

不是所有元素都支持Size Containment的,不支持Size Containment的元素包括设置了display:contentsdisplay: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指的是“布局限制”,可以想象成对元素的骨架、框架或者渲染盒子进行了封闭,形成了一个真正意义上的“结界”。这些限制会给元素带来很多和普通元素不一样的特性。

  • 会形成一个全新的包含块,无论是绝对定位元素还是固定定位元素的lefttop偏移都会相对于这个包含块元素计算。transform属性也有这个特性,在transform属性值不是none的元素中的固定定位元素的样式表现如同绝对定位。contain:layout也可以让设置了position:fixed的固定定位元素像绝对定位元素那样表现,即lefttop值是相对于设置了contain:layout的元素偏移的,而不是相对于浏览器窗体的,并且元素可以滚动,而不是位置固定。
  • 会创建一个全新的层叠上下文,除了可以改变元素重叠时的层级表现,还可以限制混合模式等CSS特性的渲染范围。
  • 会创建一个新的块状格式化上下文(Block Formatting Context,BFC),因此,Layout Containment状态下的元素是不会受到浮动元素干扰的。

Layout Containment还有下面这些大家可以不用在意的特性。

  • 如果overflow的属性值是visibleclip或两者的组合,则元素内任意元素内容的溢出都不会影响外部元素的布局。
  • 内部的元素可以分栏、分区,但是不能传播到父元素。
  • 基线消失,或者可以认为底边缘是基线。

和Size Containment类似,Layout Containment同样对隐藏元素、表格元素(不包括table-cell元素)和纯内联元素无效。

(3)Style Containment

Style Containment和很多人预想的不一样,并不是指常规的样式限制,而是指CSS计数器和其他相关内容生成的限制。例如,CSS计数器属性counter-incrementcounter-set是受到整个DOM树中的计数器影响的。

例如,父元素执行一次counter-increment,子元素又执行一次counter-incre ment,则最终的计数值是父、子元素的累加值。但是,如果设置了Style Containment,则计数范围就会被限定在元素的子树上,而不是整个树。

举个例子,HTML和CSS代码如下:

<div></div>
body {
    counter-reset: n 2;
}
div {
    contain: style;
    counter-reset: n;
}
div::before, div::after {
    content: counters(n, '.') " ";
}
div::after {
    counter-increment: n 2;
}

在这个例子中,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-quoteclose-quoteno-open-quoteno- close-quote

content内容生成虽然很实用,但是很遗憾,目前除了基本的数字序号生成,平常很少见到content属性的其他高级应用,因此,Style Containment也被拖累成一个出场机会很少的限制特性。目前Firefox浏览器并不支持Style Containment,因此我不看好Style Containment的未来。

(4)Paint Containment

就表现而言,Paint Containment和Layout Containment有不少相似之处,都会成为绝对定位和固定定位元素的包含块,会创建新的层叠上下文和格式化上下文

当然,它们的不同之处也很明显,那就是Paint Containment不会渲染任何包含框以外的内容,哪怕overflow属性值是visible;并且Paint Containment依然会保留溢出内容对布局的影响(例如会改变元素的基线位置)。请看下面这个例子:

<button>基线对齐</button>  
<p>感谢大家购买《CSS新世界》,如果你觉得内容不错,欢迎分享给周围的小伙伴。</p>
p {
    display: inline-block;
    width: 150px; height: 36px;
    padding: 10px;
    background: skyblue;
}

此时,按钮就会和<p>元素保持基线对齐,而此时<p>元素的基线就是内部文本的基线,于是会有图13-24所示的效果。

图13-24 按钮和大段文字默认的基线对齐表现示意

接下来继续给<p>元素设置contain:paint,CSS代码如下:

p {contain: paint;}

结果溢出容器的文字内容直接不渲染了,表现为透明不可见,但是大家可以发现,<p>元素的基线位置依旧和之前一样,效果如图13-25所示。

图13-25 contain:paint不渲染容器外的文字内容示意

这就是contain:paintoverflow:hidden隐藏容器外元素重要的区别之一。

2.contain属性的语法

明白了CSS Containment的4种限制类型,学习contain属性的语法就轻松多了。

contain属性的语法如下:

contain: none;
contain: strict;
contain: content;
contain: [ size || layout || style || paint ]

先讲一下这一语法中的几个关键点。

  • Size Containment限制类型对应属性值size,不妨就称为size类型。
  • Layout Containment限制类型对应属性值layout,不妨就称为layout类型。
  • Style Containment限制类型对应属性值style,不妨就称为style类型。
  • Paint Containment限制类型对应属性值paint,不妨就称为paint类型。

此时,strictcontent这两个属性值的含义就一目了然了。

  • strict表示对除style类型以外的类型都进行限制。此属性值等同于contain: size layout paint的设置。
  • content表示对除sizestyle类型以外的类型都进行限制。此属性值等同于contain: layout paint,表现为元素内内容渲染,元素外内容不渲染。

大家可以根据合适的场景选择合适的contain属性值来优化CSS渲染的性能

兼容性

contain属性本身是一个增强体验的CSS属性,因此无论兼容性如何,我们都可以放心大胆地在实际项目中使用。

目前Safari浏览器还没有支持contain属性,Chrome和Firefox浏览器均支持contain属性(准确地讲,Firefox是绝大部分支持)。该属性完整的兼容性信息如表13-13所示。

表13-13 contain属性的兼容性(数据源自Caniuse网站)

Internet Explorer logo
IE
Edge logo
Edge
Firefox logo
Firefox
Chrome logo
Chrome
Safari logo
Safari
Safari logo
iOS Safari
Android Browser logo
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代码给自己的项目做一个测试,看看效果如何:

article {
    content-visibility: auto;
    contain-intrinsic-size: 1000px;
}

其中contain-intrinsic-size属性可以理解为内容的占位尺寸。

由于content-visibility属性目前尚未成熟,因此暂时就说这么多。

[1] GPU即图形处理器,是与处理和绘制图形相关的硬件。GPU是专为执行复杂的数学和几何计算而设计的,可以让CPU从图形处理的任务中解放出来,从而执行其他更多的系统任务,如页面的计算与重绘。