翻译:《Everything You Need to Know About the CSS will-change Property》


介绍(Introduction)

如果在以 WebKit 为内核的浏览器里,对元素施加了一些样式,尤其是 CSS 3 中的 transform 和一些动画效果,你观察到了页面上有一些「闪烁」,那么你可能已经在自己没意识到尝试过了「硬件加速」。

CPU、GPU 和硬件加速

概括地来说,硬件加速意味着:在浏览器加载页面的时候, GPU(Graphics Processing Unit)帮助处理了大部分本该 CPU(Central Processing Unit)处理的重活累活。当 CSS 的执行激活了硬件加速,最直观的感受就是,页面整体的渲染速度变得非常快。

CPU 和 GPU 都是处理器。CPU 的位置在电脑主板上,它作为计算机的「大脑」负责处理基本所有电脑上需要计算的任务。GPU 的位置则在电脑的显卡上,主要负责处理和渲染所有需要显示的在计算机屏幕上的内容。此外,从设计之初被赋予的任务而言,CPU 更多的是负责处理数学运算,而 GPU 则是负责几何运算。因此,在移动设别上将原本 CPU 负责的任务给 GPU 处理,除了可以使让 CPU 的计算负担减轻不少之外,设备本身也会获得更好的性能。

浏览器渲染过程中层模型(layering model)是硬件加速(也被成为 GPU、显卡加速)的基础。当例如 3D transform 这类特别的 CSS 样式被施加、执行到页面的元素上,这个元素就从许多元素共享的一个「层」中脱离出来,拥有了一个只有它自己的「层」,可以在自己的独立空间里进行渲染,而且也不需要和别的元素同时被合成到屏幕上,稍稍滞后也没关系。这样一来,如果这个被独立出来的元素在之后,因为 transform 属性值的变化再次发生变化,页面上已经渲染完毕的元素(没有被硬件加速的那些),也不需要再次进行重新渲染,这种行为往往可以带来非常大的渲染速度的提升,无论是被激活硬件加速的元素(有了独立的渲染空间),还是没有被激活的那些(没有变化,不需要重新渲染)。这里值得一题的是,只有 3D transform 才有资格激活硬件加速,2D transform 不行。

CSS 里的 animation,transform 和 transition 并不会自动激活硬件加速,只会由浏览器内部相对较慢的渲染引擎来执行渲染任务。然后,浏览器会因为一类特比的属性被施加了从而会激活硬件加速。比如说 opacity 这个属性就是其中之一,因为 GPU 操作可以轻而易举的操作这个属性。比如说有一个层,开发者想要通过 transitionanimation 配合 opacity 来达到渐入渐出的效果,浏览器在这里就会聪明地将这个任务交给 GPU 去处理,并且在那里执行所有的操作,结果是整个操作执行的过程会变得非常快。在所有 CSS 属性中,opacity 是绝对不会出任何问题,最最高性能的属性。而另一个常用来激活的硬件加速的属性则是 3D transform。

过去常用来激活硬件加速的 translateZ()(或者 translate3D)

很长一段时间以来,身为开发者的我们都在使用 translateZ() (或者 translate3d())的方法(有时候也被称为「null transform」)来让浏览器将我们的 animation 和 transform 操作进行硬件加速。具体的操作的是对一个并不需要进行 3D 变幻的对象施加 3D 变化的属性。比如一个只会在二维空间进行动画的元素,可以施加如下的样式属性,便可以激活硬件加速:

Hardware-accelerating an operation results in the creation of what is known as a compositor layer that is uploaded to and composited by the GPU. 但是强制图层在浏览器渲染的时候进行硬件加速,可能并不能改善一些页面上性能瓶颈。新建层可能会让网页的渲染速度,但同时也有需要付出的代价:越是多的元素被激活硬件加速,会占用更多系统的内存就越是多(在移动设备上这一点的影响显得尤其大),所以在使用的时候,一定要明智的选择是否对元素进行已经加速,加速了是否真的可以提升网页性能,并且确认页面已经存在的性能瓶颈不是因为别的原因所造成。

为了避免以上这种不断在浏览器为元素创建层带来的影响,诞生了一个新的 CSS 属性。使用这个新的样式属性,可以让我们更加方便地提前告诉浏览器:我们将会对一个元素做怎么样的操作,以便浏览器也可以提前针对那些将会发生改变的元素做优化,对诸如 animation 之类可能会需要消耗不少性能的操作,在 animation 真正执行之前做出准备。这个新的 CSS 属性叫做 will-change

全新的 will-change 属性

will-change 属性的使用,可以提前告诉浏览器开发人员将要对什么元素进行什么样的变化,所以浏览器也可以提前对元素进行适当的优化,therefore avoiding a non-trivial start-up cost which can have a negative effect on the responsiveness of a page. 元素的改变和渲染速度都会变得更快,网页因而也会显得更加生机勃勃,结果是用户会获得更加顺畅的浏览体验。

比如说对一个元素使用 3D Transform 属性,就像之前提到的,在他们真正出现在显示器上之前,元素会被硬件加速,在内部会被独立到自己的一个渲染层上。然后这个操作,将元素独立到自己单独所在的渲染层上这一个操作,很可能会使得所设置的 Transform 变化发生延迟,这个延迟也很可能是肉眼可见的一秒或者几分之一秒,结果就会造成页面的闪烁。

为了避免这种闪烁,开发者需要在这些改变发生之前告诉浏览器,浏览器从而可以对元素将要发生的变化做出准备,当元素的变化真正产生变化的时候,为元素所开辟的那个层就已经准备好了,需要做的动画等操作直接可以执行,页面的渲染速度也随之变快。

使用 will-change 来提示告诉浏览器将要发生的变化非常简单,只需要在发生改变的元素上多添加一个属性(比如将要发生 transform 的变化,就如下):

开发者也可以针对元素的任何样式表属性进行 will-change 的声明,如滚动位置(scroll position)、内容等任何样式表中存在的属性名。如果想要对一个元素的多个属性进行将要变化的声明,也没问题,只是书写的格式需要一些改变:多个属性以逗号的方式相隔。比如说,如果想要对一个元素进行动画和位置上的变化,可以以如下的方式进行声明:

由于浏览器会针对被施加了 will-change 属性的元素做出特别的优化,所以开发者在使用这个属性的时候也一定要谨慎,必须谨记,如果可以不依靠这种迫使浏览器单独为元素的渲染层的方法,就可以达到整个页面渲染速度的提升,这种情况下,也应该避免使用 will-change 属性。

will-change 属性仅仅只是提示浏览器元素将要发生改变吗?

答案是肯定的,同时也是否定,需要视情况而定,取决于具体 will-change 的是什么属性。如果一个属性的非初始值会对让元素产生一个 stacking context,那么在 will-change 中声明这个属性,对元素也会造成产生 stacking context 的结果。

比如说,拥有 clip-path 和 opacity 这两个属性的任何非初始值元素都会产生 stacking context。在 will-change 属性中指定这两者之一(或者两者都指定),在元素真的发生任何变化之前,元素就会产生 stacking context。这一点对于任何别的会造成 stacking context 产生的属性都适用。

一些属性还会为定位了的元素创建 containing block,比如说规定了 transform 属性的元素会为它所有的子孙元素都创建 contianing block,甚至是那些仅仅设置了 position: fixed 样式属性的元素。所以,如果一个属性会导致 containing block 的产生,那么将这个属性设置为 will-change 的值,对 position: fixed 的元素来说也会造成 containing block 的产生。

最后需要说明的是 will-change 在浏览器渲染过程中图层合成之前的一些影响。浏览器借助 CPU 做到的次像素抗锯齿(subpixel antialiasing ),GPU 并不能胜任,由于这一点,尤其是显示文字的时候会造成模糊不清楚的视觉感受。

除此之外,设置了 will-change 属性的元素,除了告诉浏览器要对其会发生的改变做出优化之外,不会有任何别的作用。除了会造成 stacking context 和 containing block 之外,不会有任何其它效果。

使用 will-change 属性时需要做的和不能做的

上面说了那么多关于 will-change 会对元素和浏览器产生的影响,很可能会对让开发者倾向于认为;「好吧,那么就让浏览器为我优化页面里所有的元素吧!」,这种想法其实没什么不对,谁不希望那些需要改变的元素在改变之前就已经提前优化,在需要使用的时候完全按部就班各就各位达到了自己的最佳状态。

和其它任何强有力的属性一样,will-change 属性本身就带着不能滥用的枷锁。一般而言,will-change 需要巧用,否则对性能的过分要求很有可能会让页面崩溃。

和其它用来提示性能的属性一致,will-change 也有它自己不太能被简单察觉到的副作用(毕竟,它的工作方式就是通过另一种方式去和浏览器沟通协商),所以使用的时候需要相当谨慎。以下是一些在使用 will-change 属性,并且以达到最佳页面性能时必须牢记的事项。

不要在 will-change 中设置过多属性,也不要在过多元素中设置 will-chaneg 本身

如上文提到的,对开发这来说很容易就会想对所有可能发生变化的元素及其相关属性施加 will-change,从而让浏览器为我们提前做优化;按照这样的想法,下面这种属性设置方法看上去并没有什么问题

这种写法看上去不错(在第一次看到这种写法的时候我也觉得没啥问题),但实际上对性能的损害很大,而且,这样的写法不合法。不合法这点不只是因为 will-change 不能设置在全部(all)上,而且这样以逗号分隔的一堆元素上也不被认为是合法。你可以看到,浏览器已经尝试为所有它可能涉及到的范畴去做所有可以做的优化(opacity 和 3d transform 属性), 所以告诉浏览器去优化所有的属性并没有什么实际意义。而且事实是,那些浏览器用来为 will-change 属性做的更进一步的优化常常会耗费更多的资源,将它施加在所有属性上显然是一个浪费,更甚者非常过度的使用可能会造成页面相应速度的变慢或者直接崩溃。

给浏览器足够的时间去做准备

从 will-change 属性的命名上使用的将来时显然是有明确的原因:这是一个用来提前告诉浏览器将会发生改变的属相。重点在将会这个词上,并不是正在发生的改变。使用 will-change,我们是在让浏览器针对将要发生的变化做出优化,而为了足够的优化,浏览器需要充分的时间。才能够保证在变化真正发生的时候,优化也能够没有任何延迟、实时的作用于元素本身。

同时也得指出,在元素马上就要改变之前设置 will-change 属性几乎等于什么都没干(效果也有可能比将 will-change 属性设置到全部上好不到哪里去。比如说在使用动画的时候,在动画发生前一瞬间设置 will-change,很可能会导致需要创建第二个用于展示动画的层)。比如下面这个在 :hover 发生时候的动画

在这个例子中,设置 will-change 属性的时机,就相当于告诉浏览器去为已经发生了的属性的变化做出优化,不但没有任何实际效果,同时也背离了 will-change 设计产生的初衷。所以,开发者需要多多少少为优化提前些时间去告诉浏览器将要发生的改变,在一个合适的地方设置 will-change。

举例来说,如果一个元素在(鼠标)点击的时候会发生变化,那么在(鼠标)悬浮状态的时候加入 will-change 属性就可以让浏览器有足够的时间来为将要发生的变化做出优化。用户(鼠标)在悬浮和点击之间的时间对浏览器来说已经完全足够,因为人类的反应时间相对来说还是很慢的,在这个例子中,悬浮和点击两个状态之间,在点击发生之前,浏览器可以获得差不多 200ms 的时间,而对浏览器来说这个时间长度对需要作出的优化已经绰绰有余。

但如果开发者想要发生的变化是发生在悬浮状态,而非点击状态呢?在这种情况下上面这种方法就没有任何意义、也毫无用处。在这种情况下,要猜测变化的具体发生之前的时间点也不是不可能。比如,找出当前元素的上级元素的悬浮状态,就可以为当前元素的悬浮状态流出足够时间的优化时间:

然后,通过元素的上层元素并不是每次都能成功指定当前元素,所以开发者可以选择不同的方式来实现这个需求,比如一个视图被激活了的时候,或者明确元素确确实实在 viewport 范围内的时候,以上这些指标都可以大大增加需要施加属性元素被发现的可能性。

在变化发生之后移除 will-change

浏览器对元素将要发生的改变所作出的优化是非常昂贵的,而且就像上文指出过的,同时也会占用大量硬件资源。这里需要提一句,浏览器优化的处理过程,尤其在最后,当需要做的优化处理完成之后,浏览器会尽可能迅速地将这些优化所做的措施移除,将一切返回正常状态。然后,浏览器尽快将一切恢复到平常状态的做法,当施加了 will-change 属性之后,就被推翻了,至少,优化措施会占用比普通优化更长的时间。

所以,开发者需要谨记,当每次需要 will-change 产生的效果完成之后,得将 will-change 属性本身移除,以此来释放浏览器为做出的优化而占用的资源。

通过样式表来声明的 will-change 属性是不可能被移除的,以此几乎在所有的情况下,都鼓励开发者通过 JavaScript 来设置和移除 will-change 属性。通过使用脚本的方式,开发者可以在元素发生变化前的合适时机声明 will-change,而在需要的效果结束后,将其移除。以上一章节中的例子来说,开发者可以通过监听元素的鼠标悬浮(hover)事件,然后再 mouseenter 事件发生时设置 will-change 属性。在动画的结束的时候,通过 DOM 的 animationEnd 事件来讲 will-change 属性移除。

Craig Buckler 写了一篇关于通过 JavaScript 捕捉 CSS 中 animation 事件的文章,如果对此不熟悉可以参考。CSS-Tricks 上也有一篇关于控制 CSS 中 animation 和 transitions 的文章也值得阅读。

在样式表文件中尽可能保守地使用 will-change 属性

就像之前章节中说的那样,will-change 是用来提前几百、几千毫秒告诉浏览器对特别元素、属性进行优化的属性。通过这样的方法使用 will-change 的时候,在样式表中对元素直接进行声明并没有问题,虽然更多的会推荐使用脚本来对其进行控制。以下是一些当在样式表中设置 will-change 属性时合适做法的概括。

一个适合的使用对象是那些用户会反复交互、并且会需要给出快速反应的元素。对这些元素需要有数量上的限制,以数量上的限制保证浏览器所作出的优化不会被过度使用从而造成负面效果。比如说项目中如果有一个从侧边划出的菜单,像如下这般对属性进行设置就会比较合适:

另一种合适的使用场景是,元素似乎在持续不断的变化,比如说有一个交互效果是有特定元素对鼠标在屏幕上移动做出反应,只要用户在屏幕前、手持鼠标,那么鼠标就是在持续不断地运动。在这个例子中,直接将 will-change 通过样式表文件为元素进行设置就非常合适,浏览器应该做出持续的优化。

will-change 属性的值

will-change 有四个可能值:autoscroll-positioncontents<custom-ident>

<custom-ident> 的值用来指定将要变化的那些属性,一个或多个,如果是多个属性则使用逗号进行分隔。以下是一些基本用法的例子。

在 will-change 定义为 的时候,排除了 will-change, none, all, auto, scroll-position 和 contents 这些关键词。所以向我们开头提到的那样,will-change: all 这样的声明完全没有意义也没有作用,会直接被浏览器忽略。

auto 值的作用是不特指任何样式属性需要被优化,这个情况下浏览器几乎只会做出普通情况下所做的那些优化。

scroll-position 值的作用显而易见,优化那些滚动位置会发生变化的元素 scroll-position。这个值很有用处,因为当设置了这个值之后,浏览器会对那些即使当前在窗口中没有得到显示的元素做出准备,提前进行渲染优化。浏览器对一个窗口中内容的渲染过程,常常是只针对当前窗口中的内容,以及那些滚动经过窗口的内容进行渲染,这样一来可以平衡渲染内存的使用和渲染所需的时间。而当使用 will-change: scroll-position 的时候,可以对滚动的内容作进一步优化,使得更长的内容和(或者)更快滚屏速度可以丝滑顺畅。

content 值是用来暗示元素的内容将会发生变化。浏览器通常会把元素的渲染结果「缓存」下来,因为页面上的内容大部分都不会经常发生变化,或者变化只会发生在元素的位置上。当遇到 will-change: content 声明时,浏览器对该元素的缓存就减少,或者彻底不做缓存。因为元素的内容如果一而再再而三地频繁发生变化,在本地保存一份元素的缓存显然好无用处并且浪费时间,浏览器本身在这个情况下就不如选择停止任何缓存的动作,当元素发生变化就立即重新渲染。

上文中也提到,在 will-change 中有一些属性的声明并不会起到任何作用,因为浏览器本身不会对那些属性做出任何特别的优化,虽然如此,但是声明这些属性也不会造成什么负面效果,结果只会是没有任何结果–被浏览器彻底忽略而已。另一些属性则会造成 stacking context(opacity, clip-path 等等)和(或者)containing blocks。

浏览器支持

目前是 2015 年 8 月,当前 Chrome 46+,Opera 24+ 和 Firefox 36+ 已经对 will-change 属性做了支持, Safari 则正在实现,同时微软的 Edge 则将它列为了「正在考虑」状态。

结语

will-change 的主要作用是帮助我们实现不需要 heck 就可以达成的性能上的优化,并且强调 CSS 样式操作在浏览器渲染速度和性能上的重要性。但是,拥有极大权力的同时也被赋予了重大的责任,will-change 属性的使用必须非常谨慎。到这里就不得不引用 Tab Atkins Jr.(will-change 规范文档的编辑)的话:

在确实会发生改变和正在发生变化的元素上设置 will-change 属性,同时在变化完成之后马上进行移除操作。

感谢阅读。

非常感谢 Paul Lewis 对这篇文章的检阅和反馈,Tab Atkins 的支持和对我提出问题的回答,还有 Bruce Lawson 和 Mathias Bynens 对文章的阅读。

感谢阅读

你们好, 2018 年初把小站从 Jekyll 迁移到 Hugo 的过程中,删除了评论区放的 Disqus 插件,考虑有二:首先无论评论、还是对笔记内容的进一步讨论,读者们更喜欢通过邮件、或者 Twitter 私信的方式来沟通;其次一年多以来 Disqus 后台能看到几乎都是垃圾留言(spam),所以这里直接贴一下邮件、以及 Twitter 账户 地址。

技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。

BEST
Lien(A.K.A 胡椒)
本站总访问量 本站总访客量 本文总阅读量