【CSS】使用will-change来提高页面的渲染速度

5,542 阅读16分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。


原文:Everything You Need to Know About the CSS will-change Property

译者:Mancuoj

简介

如果你曾经注意到过基于WebKit的浏览器在执行某些CSS操作(尤其是transformanimation)时出现的“闪烁”,那么你很可能接触过“硬件加速”这个术语。

CPU、GPU和硬件加速

简而言之:硬件加速意味着图形处理单元(GPU)会协助你的浏览器渲染网页,做一些繁重的工作,而不是把这些工作全部丢给中央处理单元(CPU)来完成。当一个CSS操作被硬件加速时,它的速度也会因为页面的渲染速度变快而得到提升。

正如它们的名字所示,CPU和GPU都是处理单元。CPU位于计算机的主板上,它被称为计算机的🧠。GPU位于计算机的显卡上,负责处理和渲染图像。此外,GPU是专门为执行复杂的数学和几何计算而设计的,这些计算也是图形渲染的必要条件。因此,将操作丢给GPU处理可以产生巨大的性能提升,也可以减少移动设备上的CPU争用。

硬件加速(又称GPU加速)依赖于浏览器渲染页面时使用的分层模型。当对页面上的元素进行某些操作(比如3D变换)时,该元素会被移动到属于它自己的“图层”,在那里它可以独立于页面的其他部分进行渲染,并在之后被合成(画到屏幕上)。这样就隔离了内容的渲染,如果该元素的变换是帧之间唯一的变化,那么页面的其他部分就不必重新渲染,这样往往能够提供明显的速度优势。不过这里需要说明一下:只有3D变换拥有属于自己的图层,2D变换没有。

CSS的animationtransformtransition不会自动进行GPU加速,一般是由浏览器缓慢的软件渲染引擎执行。然而有些浏览器通过某些属性提供硬件加速,以获得更好的渲染性能。

例如,opacity不透明度就是少数可以适当加速的CSS属性之一,因为GPU可以很容易地操纵它。基本上,如果你在transitionanimation里淡化不透明度,浏览器会智能地把它丢给GPU操作,速度会非常快。不透明度是所有CSS属性里性能最好的之一,你使用它不会出现任何问题。其他常见的硬件加速操作是CSS的3D变换。

The Old:translateZ()(or translate3d())Hack

很长时间以来,我们一直在使用 translateZ() (or translate3d()) 这种黑客行为(有时也称为null transform hack)来欺骗浏览器animationtransform推给硬件加速。

通过给一个不会在三维空间变换的元素添加一个简单的三维变换来实现。例如一个二维空间的元素可以添加这个简单的规则来进行硬件加速:

transform: translate3d(0, 0, 0);

硬件加速操作会创建一个所谓的合成渲染层(compositor layer),并上传到GPU进行合成。然而,强行创造一个图层不一定能解决某些页面上的性能瓶颈。

虽然图层创建技术可以提升页面速度,但有一定代价:它们会占用系统RAM和GPU上的内存(在移动设备上很有限),而且拥有大量的图层会产生不好的影响(尤其是在移动设备上),所以必须明智地使用它们,确保其可以真正帮助页面性能,而且性能瓶颈不是由你页面上的其他操作造成的。

为了避免图层创建的黑客行为,我们引入了一个新的CSS属性will-change,它允许我们提前告知浏览器我们可能会对一个元素做出什么样的改变,从而使浏览器可以提前为元素做一些适当的优化。例如,在动画实际开始之前进行一些潜在的大量的准备工作。

The New:值得称道的will-change

will-change属性允许你提前告知浏览器你可能会对一个元素进行什么样的改变,这样它就可以提前设置适当的优化,避免了可能会对页面的响应性产生负面影响的启动成本。这些元素可以更快地被改变和渲染,页面将能够迅速地更新,从而带来更流畅的体验。

例如,当在一个元素上使用CSS三维变换时,该元素及其内容可能会升到一个新的图层,就像前面提到的那样,之后才会被合成(绘制到屏幕上)。然而,在一个新的图层中设置元素是一个代价相对昂贵的操作,这可能会使变换动画的开始时间延迟几分之一秒,导致明显的“闪烁”。

为了避免这种延迟,你可以在变化实际发生前的一段时间通知浏览器。这样,浏览器就会有一些时间为这些变化做准备,当这些变化发生时,元素的图层就会准备好,变换动画就可以执行,然后元素就可以被渲染,页面就会迅速更新。

使用will-change,向浏览器提示即将发生的变换,可以在你期望被变换的元素上添加这个简单的规则:

will-change: transform;

你也可以向浏览器声明,你打算改变一个元素的滚动位置(该元素在可见滚动窗口中的位置,以及在该窗口中的可见程度)、或者改变其内容、一个或多个CSS属性值,方法是指定你期望改变的属性的名称。如果你期望或计划改变一个元素的多个项,你可以提供一个逗号分隔的值的列表。

例如,如果你将会改变元素位置,你可以这样向浏览器声明:

will-change: transform, opacity;

指定你想要改变的具体内容,可以让浏览器更好地优化为这些特定的变化。这显然提升速度的一个更好的方法,而不必诉诸于hack以及强迫浏览器进行不必要图层的创建。

除了对浏览器的渲染提示之外,will-change 会影响它声明的元素吗?

会或者不会取决于你所声明并告知浏览器的属性。如果一个属性的任何非初始值都会在该元素上创建一个stacking context ,那么在will-change中指定该属性将在该元素上创建一个层叠上下文

译者注:如果一个元素含有层叠上下文,我们可以理解为这个元素在z轴上的level更高一级。换句话说就是于网页中元素级别更高,离用户更近了。

例如,clip-pathopacity属性,当它们的值不是初始值时,都会在被应用的元素上创建一个层叠上下文。因此,使用这些属性中的一个(或两个)作为will-change的值会在元素上创建一个层叠上下文,甚至在变化实际发生之前创建。这同样适用于其他会在元素上创建层叠上下文的属性。

另外,一些属性可以导致为固定位置的元素创建一个包含块。例如,一个被转换的元素为其所有定位的子元素创建一个包含块,即使是那些已经被设置为position: fixed的元素。所以,如果一个属性导致了一个包含块的产生,那么把它指定为will-change的值也会导致固定位置元素产生一个包含块。

译者注:一般情况下,CSS中一个元素的位置和尺寸的计算都相对于一个矩形,这个矩形被称为包含块。

如前所述,will-change的一些变化会导致创建一个新的合成层。然而,GPU并不支持大多数浏览器中CPU所做的子像素抗锯齿,有时会导致内容模糊(尤其是文字)。

除此之外,will-change属性对它所指定的元素没有直接影响,它只是对浏览器的一个渲染提示,允许它为该元素发生的变化进行优化设置。除了在上述情况下创建堆叠上下文和包含块之外,它对元素没有直接影响。

will-change注意事项

知道了will-change的作用后,你可能会想:“只要让浏览器优化所有的东西就行了!” 这个想法很合理,谁不希望他们所有的变化都被优化,并在需要时准备好?

尽管will-change很🐂🍺,但它与其他种类的“权力”没有任何区别,滥用它会导致性能上的打击,可能使你的页面崩溃。

与任何优化一样,will-change也有其不能直接察觉的副作用(毕竟,它只是一种在幕后与浏览器对话的方式),所以它的使用可能很棘手。这里有一些你在使用这个属性时需要注意的事情,以确保你能得到它的最佳效果,同时避免滥用它可能带来的伤害。

不要声明太多元素

正如我前面提到的,如果告诉浏览器对可能发生在所有元素上的所有属性的变化进行优化:

*,
*::before,
*::after {
	will-change: all;
}

尽管这看起来很好,而且一开始来说很有意义,但事实上这非常有害,更多的操作是无效的。不仅all关键字是will-change的无效值(我们将在文章后面介绍有效和无效值的列表),而且这样一揽子规则也并没有用。

浏览器已经尽可能地优化了一切(还记得opacity和3D变换吗?),所以这样做并没有真正改变什么,也没有任何帮助。事实上,这样做有可能会占用机器的大量资源,如果过度使用,会导致页面变慢甚至崩溃。

换句话说,让浏览器对可能发生也可能不发生的变化保持准备状态而不是什么好事。Don’t do it.

给浏览器足够的时间

will-change属性之所以叫做will是因为:通知浏览器即将发生的变化,而不是正在发生的变化。

使用will-change,我们要求浏览器对我们所声明的变化进行某些优化,为了实现这一目标,浏览器需要一些时间来实际进行这些优化,这样,当变化发生时,优化就可以毫无延迟地应用。

在一个元素发生变化之前立即对其设置will-change,几乎没有任何效果。(它实际上可能比不设置更糟,可能会有一个新图层的成本,而你正在做的动画以前并没有资格做一个新层!)

例如,如果一个变化将在悬停时发生:

.element:hover {
	will-change: transform;
	transition: transform 2s;
	transform: rotate(30deg) scale(1.5);
}

…告诉浏览器为已经发生的变化进行优化是没有用的,这样做是否定了will-change背后的整个概念。相反,你应该找到一种方法,至少可以稍微提前预测会发生变化的东西,并在那时设置will-change

例如,如果一个元素在被点击的时候会发生变化,那么就在该元素被悬停的时候设置will-change,这样就给了浏览器足够的时间来优化。从用户悬停该元素到实际点击该元素之间的时间足以让浏览器进行优化设置,因为人类的反应时间相对较慢,所以在变化实际发生之前,浏览器会有大约200ms的时间窗口,这足以让它进行优化设置。

.element {
	transition: transform 1s ease-out;
}
.element:hover {
	will-change: transform;
}
.element:active {
	transform: rotateY(180deg);
}

但如果你希望变化发生在悬停时,而不是点击时呢?上面的声明将是无用的,在这种情况下,往往还是可以找到一些方法来预测动作发生之前的情况。

例如,悬停变化元素的祖先可能会提供足够的准备时间:

.element {
	transition: opacity .3s linear;
}

/* 当鼠标进入或悬停在其祖先时,声明该元素的变化 */
.ancestor:hover .element {
	will-change: opacity;
}

/* 当元素被悬停时的变化 */
.element:hover {
	opacity: .5;
}

然而,悬停祖先并不总是表明该元素肯定会被交互,所以你可以做一些事情,比如当视图在你的应用程序中变得活跃时,或者如果该元素在视口的可见部分内,设置will-change,这将增加该元素被交互的机会。

更改完成后删除

浏览器为即将发生的变化所做的优化通常会占用机器的很多资源,通常要删除这些优化尽快恢复到正常行为。然而,will-change覆盖了这一行为,它维持优化的时间比浏览器所做的要长很多。

因此,你应该始终记得在元素变化完成后删除will-change,这样浏览器就可以恢复优化所占用的资源。

如果在样式表中声明了will-change,就不可能删除它,这就是为什么建议你用JavaScript设置和取消它。通过脚本,你可以向浏览器声明你的修改,然后在修改完成后,通过监听这些修改完成的时间来删除will-change

例如,就像我们在上一节的样式规则中所做的那样,你可以监听元素(或其祖先)何时被悬停,然后在鼠标进入时设置will-change。如果你的元素正在被动画化,你可以使用DOM事件animationEnd来监听动画何时结束,然后在animationEnd被触发时移除will-change

// 一个例子
// 获取点击时将被动画化的元素,例如
var el = document.getElementById('element');

// 设置元素被悬停时的变化
el.addEventListener('mouseenter', hintBrowser);
el.addEventListener('animationEnd', removeHint);

function hintBrowser() {
	// 在动画的关键帧中要改变的可优化的属性
	this.style.willChange = 'transform, opacity';
}

function removeHint() {
	this.style.willChange = 'auto';
}

Craig Buckler写了一篇关于在JavaScript中捕捉CSS动画事件的文章,如果你不熟悉这个,可以去看看。CSS-Tricks上也有一篇关于控制CSS动画和过渡的文章,也值得一看。

在样式表中谨慎使用

正如我们在上一节中所看到的,will-change可以用来提示浏览器在几毫秒内某个元素即将发生的变化。这就是在样式表中声明will-change的用例之一。尽管我们建议使用JavaScript来设置和取消will-change,但在某些情况下,在样式表中设置它(并保持它)是合适的。

一个例子:在一些元素上设置will-change,这些元素可能会被用户反复交互,并且应该以一种快速的方式响应用户的交互。有限的元素数量意味着浏览器所做的优化不会被过度使用,因此不会有太大的伤害。

例如,通过在用户要求时将侧边栏滑出的方式来改造它。下面的规则就很合适:

.sidebar {
	will-change: transform;
}

另一个例子:在不断变化的元素上使用will-change,比如一个响应用户鼠标移动的元素,随着鼠标的移动在屏幕上移动。在这种情况下,只需在样式表中声明will-change的值就可以了,因为它准确地描述了该元素将定期/不断地变化,所以应该保持优化。

.annoying-element-stuck-to-the-mouse-cursor {
	will-change: left, top;
}

属性值

will-change有四个属性值:autoscroll-positioncontents<custom-ident>

<custom-ident> 值用于指定你期望改变的一个或多个属性的名称。多个属性用逗号隔开。

下面是带有指定属性名称的有效的will-change声明的例子。

will-change: transform;
will-change: opacity;
will-change: top, left, bottom, right;

<custom-ident>:排除了关键字will-changenoneallautoscroll-positioncontents,还有一些通常被排除在外的关键字。所以,正如我们在文章开头提到的,will-change: all的声明是无效的,会被浏览器忽略。

auto并没有特别的意思,除了通常的优化外,浏览器不会设置任何特别的优化。

scroll-position:顾名思义,表示你希望在不久的将来随时改变一个元素的滚动位置。这个值很有用,在使用时浏览器会准备并渲染可滚动元素上滚动窗口中可见内容之外的内容。浏览器通常只渲染滚动窗口中的内容,以及超过该窗口的部分内容,以平衡因跳过渲染而节省的内存和时间以及使滚动看起来更漂亮。使用will-change: scroll-position,它可以做进一步的渲染优化,从而使更长或更快的内容滚动可以顺利地完成。

contents:预计该元素的内容会发生变化。浏览器通常会随着时间的推移缓存元素的渲染,因为大多数东西并不经常改变,或者只改变它们的位置。这个值会被浏览器解读为一个信号,即减少对该元素的缓存,或者完全避免对该元素的缓存,因为如果该元素的内容经常变化,那么保持对内容的缓存将是无用且浪费时间的,所以浏览器会直接停止缓存,只要该元素的内容发生变化,就继续从头渲染。

浏览器支持

截至2015年8月,Chrome 36+、Opera 24+和Firefox 36+支持will-change属性。

Safari目前正在实施will-change,Edge正在 “考虑中”

写在最后

will-change属性将帮助我们编写无黑客性能优化的代码,并强调速度和性能对我们的CSS操作的重要性。但是欲戴王冠,必承其重,will-change不应该被轻视,但也需要明智地使用。在这一点上,引用will-change规范编辑Tab Atkins Jr的话:

Set will-change to the properties you’ll actually change, on the elements that are actually changing. And remove it when they stop.

感谢你的阅读!