amfe / article

7.59k stars 1.07k forks source link

CSS Animation性能优化 #47

Open airen opened 8 years ago

airen commented 8 years ago

CSS Animation是实现Web Animation方法之一,其主要通过@keyframesanimation-*或者transition来实现一些Web动效。不过今天我们聊的不是怎么制作Web动画,咱们来聊聊CSS Animation性能相关的话题。

浏览器渲染原理

关于浏览器工作原理之前有一篇非常出名的文章《浏览器的工作原理:新式网络浏览器幕后揭秘》。文章详细阐述了浏览器工作原理,下面用两张图来分别描述Firefox和Chrome浏览器对Web页面的渲染过程:

Chrome渲染过程

Chrome渲染过程

有关于Chrome浏览器渲染的详细内容,可以参考《图解浏览器渲染过程 - 基于Webkit/Blink内核Chrome浏览器》一文。

Firefox渲染过程

Firefox渲染过程

特别声明:接下来的内容都是针对于Chrome浏览器进行讨论。

Chrome渲染部分的实际含义

从上面的流程图中不难看出,Chrome渲染主要包括Parse Html、Recalculate Style、Layout、Rasterizer、Paint、Image Decode、Image Resize和Composite Layers等。简单了解一下其含义,以便后续内容的更好理解。

Parse Html

发送一个http请求,获取请求的内容,然后解析HTML的过程。

有一个经典的前端面试题:当你在浏览器中输入google.com并且按下回车之后发生了什么? 这个面试题或许能帮助大家更好的理解Parse Html,甚至是浏览器渲染的其他几个部分。

Recalculate Style

重新计算样式,它计算的是Style,和Layout做的事情完全不同。Layout计算的是一个元素绝对的位置和尺寸,或者说是“Compute Layout”。

Recalculate被触发的时候做的事情就是处理JavaScript给元素设置的样式而已。Recalculate Style会计算Render树(渲染树),然后从根节点开始进行页面渲染,将CSS附加到DOM上的过程。

任何企图改变元素样式的操作都会触发Recalculate。同Layout一样,它也是在JavaScript执行完成后才触发的。

Layout

计算页面上的布局,即元素在文档中的位置及大小。正如前面所述,Layout计算的是布局位置信息。任何有可能改变元素位置或大小的样式都会触发这个Layout事件。

触发Layout的属性非常的多,如果想了解什么属性会触发Layout事件,可以在CSS Triggers网站查阅。下图截了一部分:

Laout

Rasterizer

光栅化,一般的安卓手机都会进行光栅化,光栅主要是针对图形的一个栅格化过程。低端手机在这部分耗时还是蛮多的。

Paint

页面上显示东西有任何变动都会触发Paint。包括拖动滚动条,鼠标选择中文字等这些完全不改变样式,只改变显示结果的动作都会触发Paint。

Paint的工作就是把文档中用户可见的那一部分展现给用户。Paint是把Layout和Recalculate的计算的结果直接在浏览器视窗上绘制出来,它并不实现具体的元素计算。

Image Decode

图片解码,将图片解析到浏览器上显示的过程。

Image Resize

图片的大小设置,图片加载解析后,若发现图片大小并不是实际的大小(CSS改变了宽度),则需要Resize。Resize越大,耗时越久,所以尽量以图片的原始大小输出。

Composite Layers

最后合并图层,输出页面到屏幕。浏览器在渲染过程中会将一些含有特殊样式的DOM结构绘制于其他图层,有点类似于PhotoShop的图层概念。一张图片在PotoShop是由多个图层组合而成,而浏览器最终显示的页面实际也是有多个图层构成的。

下面这些因素都会导致新图层的创建:

有关于Composite方面的深入剖析,可以阅读《无线性能优化:Composite》一文。

像素渲染流水线

通过前面的介绍,在屏幕上最终呈现的页面,是类似于图层一样合并输出到屏幕上的。其实所写的Web页面最终以像素的形式在浏览器屏幕上呈现。这样一来,我们需要理解所写的页面代码是如何被转换成屏幕上显示的像素。这个转换过程可以归纳为这样的一个流水线,主要包含五个关键步骤:

像素渲染流水线

上述过程的每一步都有可能会发生,因此一定要弄清楚自己的代码将会运行在哪一步。

虽然在理论上,而面的每一帧都是结过上述的流水线处理之后渲染出来的,但并不意味着页面每一帧的渲染都需要经过上述五个步骤的处理。实际上,对视觉变化效果的一个帧的渲染,有三种常用的流水线。

JavaScript/CSS =>计算样式=>布局=>绘制=>渲染层合并

像素渲染流水线

如果你修改一个DOM元素的“Layout”属性,也就是改变了元素的样式(比如widthheight或者position等),那么浏览器会检查哪些元素需要重新布局,然后对页面激发一个reflow(重排)过程完成重新布局。被reflow(重排)的元素,接下来也会激发绘制过程,最后激发渲染层合并过程,生成最后的画面。

reflow又叫重排,是指浏览器计算页面的全部或部分布局所做的处理。reflow必定会引发重绘,这对于Web的性能影响是极大的。

JavaScript/CSS => 计算样式 =>绘制 =>渲染层合并

像素渲染流水线

如果你修改一个DOM元素的“Paint Only”属性,比如背景图片、文字颜色或阴影等,这些属性不会影响页面的布局,因此浏览器会在完成样式计算之后,跳过布局过程,只会绘制和渲染层合并过程。

JavaScript/CSS => 计算样式 =>渲染层合并

像素渲染流水线

如果你修改一个非样式且非绘制的CSS属性,那么浏览器会在完成样式计算之后,跳过布局和绘制的过程,直接做渲染层合并。这种方式在性能上是最理想的,对于动画和滚动这种负荷很重的渲染,我们要争取使用第三种渲染过程。

通过前面这么多的内容介绍,我们可以得知,影响Web性能主要过程包括Layout、Paint和Composite。那么对于CSS Animation而言,我们的所有操作都是通过CSS的样式控制动画,言外之意,只要是会触发Layout、Paint和Composite的CSS属性都会直接影响动画的性能。在CSS中所有影响Layout、Paint和Composite的属性都可以通过CSS Triggers**网站查阅。那么如何避免达到前面所述的,整个动画尽量避开重排和重绘,只做渲染层合并呢?暂且先不讨论,把这部分放到最后面来讨论。接下来接着先看看其他相关的知识点。

渲染性能

在理解渲染性能之前,我们有必要先了解前面提到的两个概念重排(也就是回流)重绘。因为这两者与前面介绍的像素渲染流水线中的LayoutPaint都有关系,而且Layout和Paint对性能的渲染又有莫大的关系。

Reflow(重排)

Reflow(重排)指的是计算页面布局(Layout)。某个节点Reflow时会重新计算节点的尺寸和位置,而且还有可能触其后代节点Reflow。在这之后再次触发一次Repaint(重绘)

当Render Tree中的一部分(或全部)因为元素的尺寸、布局、隐藏等改变而需要重新构建。这就称为回流,每个页面至少需要一次回流,就是页面第一次加载的时候。

在Web页面中,很多状况下会导致回流:

Repaint(重绘)或者Redraw遍历所有节点,检测节点的可见性、颜色、轮廓等可见的样式属性,然后根据检测的结果更新页面的响应部分。

当Render Tree中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格、而不会影响布局的。就是重绘。

将重排和重绘的介绍结合起来,不难发现:重绘(Repaint)不一定会引起回流(Reflow重排),但回流必将引起重绘(Repaint)

既然如此,那么什么情况之下会触发浏览器的Repaint和Reflow呢?

可以说Reflow和Repaint都很容易触发,而它们的触发对性能的影响都非常大,但非常不幸的是,我们无法完全避免,只能尽量不去触发浏览器的Reflow和Repaint。

从前面的内容可以了解到,Reflow和Repaint对性能影响很大,那么具体哪些点会影响到渲染性能呢?

影响Layout的属性

当你改变页面上某个元素的时候,浏览器需要做一次重新布局的操作,这次操作会包括计算受操作影响所有元素的几何数,比如每个元素的位置和尺寸。如果你修改了html这个元素的width属性,那么整个页面都会被重绘。

由于元素相覆盖,相互影响,稍有不慎的操作就有可能导致一次自上而下的布局计算。所以我们在进行元素操作的时候要一再小心尽量避免修改这些重新布局的属性。

具体有关于会影响Layout的CSS属性可以在CSS Triggers网站中查阅。

影响Repaint的属性

有些属性的修改不会触发重排,但会触Repaint(重绘),现代浏览器中主要的绘制工作主要用光栅化软件来完成。所以重新会制的元素是否会很大程度影响你的性能,是由这个元素和绘制层级的关系来决定的,如果这个元素盖住的元素都被重新绘制,那么代价自然就相当地大。

具体有关于会影响Layout的CSS属性可以在CSS Triggers网站中查阅。

如果你在动画里面使用了上述某些属性,导致重绘,这个元素所属的图层会被重新上传到GPU。在移动设备上这是一个很昂贵耗资源的操作,因为移动设备的CPU明显不如你的电脑,这也意味着绘制的工作会需要更长的时间;而上传线CPU和GPU的带宽并非没有限制,所以重绘的纹理上传就自然需要更长的时间。

CSS Triggers网站中可以得知哪些属性会触发重排、哪些属性会触发重绘以及哪些属性会触合成。但并不是CSS中所有的属性都可以用于CSS Animation和Transition中的。在W3C官方规范中明确定了哪些CSS属性可以用于AnimationTransition中。@Rodney Rehm还对这些属性做过一个兼容测试。如果你想深入的了解这方面的知识,建议您阅读下面两篇文章:

如此一来,我们知道可用于CSS Animation或者Transition的CSS属性之后,再配合CSS Triggers网站,可以轻易掌握哪些CSS属性会触发重排、重绘和合成等。虽然无法避免,但我们可以尽量控制

性能优化

如果我们知道浏览器是如何渲染一个页面的,并且去优化渲染过程中的关键步骤,不是是就能事半功倍呢?

有关于这部分的介绍,建议大家阅读《渲染性能》。

像素渲染流水线

在像素渲染流水线中,得知,如果我们能幸运的避免Layout和Paint,那么性能是最好的,言外之意,动画性能也将变得最佳。那么在CSS中可能通过不同的方式来创建新图层。其实这也就是大家常说的,通过CSS的属性来触发GPU加速。浏览器会为此元素单独创建一个“层”。当有单独的层之后,此元素的Repaint操作将只需要更新自己,不用影响到别人。你可以将其理解为局部更新。所以开启了硬件加速的动画会变得流畅很多。

为什么开启硬件加速动画就会变得流畅,那是因为每个页面元素都有一个独立的Render进程。Render进程中包含了主线程和合成线程,主线程负责:

合成线程则主要负责:

我们可以得到一个大概的浏览器线程模型:

线程

我们可以将页面绘制的过程分为三个部分:Layout、Paint和合成。Layout负责计算DOM元素的布局关系,Paint负责将DOM元素绘制成位图,合成则负责将位图发送给GPU绘制到屏幕上(如果有transformopacity等属性则通知GPU做处理)。

GPU加速其实是一直存在的,而如同translate3D这种hack只是为了让这个元素生成独立的 GraphicsLayer , 占用一部分内存,但同时也会在动画或者Repaint的时候不会影响到其他任何元素,对高刷新频率的东西,就应该分离出单独的一个 GraphicsLayer。

GPU对于动画图形的渲染处理比CPU要快。

RenderLayer 树,满足以下任意一点的就会生成独立一个 RenderLayer。

每个RenderLayer 有多个 GraphicsLayer 存在

每个GraphicsLayer 生成一个 GraphicsContext, 就是一个位图,传送给GPU,由GPU合成放出。

那么就是说,GraphicsLayer过少则每次repaint大整体的工作量巨大,而过多则repaint小碎块的次数过多。这种次数过多就称为 层数爆炸 ,为了防止这个爆炸 Blink 引擎做了一个特殊处理。

有关于这部分内容的详细介绍,可以阅读《无线性能优化:Composite》一文。

扯了这么多,我们可以稍微总结一下下:

不是所有属性动画消耗的性能都一样,其中消耗最低的是transformopacity两个属性(当然还有会触发Composite的其他CSS属性),其次是Paint相关属性。所以在制作动画时,建议使用transformtranslate替代marginposition中的toprightbottomleft,同时使用transform中的scaleX或者scaleY来替代widthheight

为了确保页面的流程,必须保证60fps内不发生两次渲染树更新,比如下图,16ms内只发生如下几个操作则是正常及正确的:

线程

页面滚动时,需要避免不必要的渲染及长时间渲染。其中不必要的渲染包括:

长时间渲染包括:

在CSS中除了开启3D加速能明显的让动画变得流畅之外,在CSS中提供了一个新的CSS特性:will-change。其主要作用就是提前告诉浏览器我这里将会进行一些变动,请分配资源(告诉浏览器要分配资源给我)

will-change属性,允许作者提前告知浏览器的默认样式,那他们可能会做出一个元素。它允许对浏览器默认样式的优化如何提前处理因素,在动画实际开始之前,为准备动画执行潜在昂贵的工作。有关于will-change更详细的介绍可以点击这里

话说回来,will-change并不是万能的,不是说使用了will-change就对动画的性能有提高,而是要正确使用,才会有所改为。在使用will-change时应该注意:

在使用will-change一定要注意方式方法,比如常见的错误方法是直接在:hover是使用,并没有告诉浏览器分配资源:

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

其正确使用的方法是,在进入父元素的时候就告诉浏览器,你该分配一定的资源:

.element {
    transition: opacity .3s linear;
}
/* declare changes on the element when the mouse enters / hovers its ancestor */
.ancestor:hover .element {
    will-change: opacity;
}
/* apply change when element is hovered */
.element:hover {
    opacity: .5;
}

另外在应用变化之后,取消will-change的资源分配:

var el = document.getElementById('demo');
el.addEventListener('animationEnd', removeHint);

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

除了will-change能让我们在制作动画变得更为流畅之外,在CSS层面上,还有别的方案吗?这个答案是肯定的。前面通过大幅的篇幅了解到,影响性能主要是因为重绘和重排。针对于这方面,CSS提供了一个新的属性contain

contain

有关于这方面的详细介绍,可以阅读《Chrome 52 中的 CSS Containment 属性》一文。

总结

本文主要介绍了浏览器渲染一个Web页面的原理,从中了解到影响Web的性能因素,从底层找到影响Web渲染性能的主要因素是CSS的属性会触发浏览器的重绘、重排。而CSS的动画,也主要是控制CSS的属性,从这一方面说明,影响动画的性能也是造成重绘和重排的CSS。为了让一个动画能更佳的流畅,我们就要从技术的手段避免CSS的属性造成浏览器的重绘和重排。以及利用一些CSS的新属性,让动画的性能更好,也就是让动画更为流畅。

文章整理的感觉有些零乱,上述内容如果有不对之处,还希望大婶们多多指点。

参考资料

xyyjk commented 8 years ago

补充几个来自Google Developers的原文:

TingYinHelen commented 5 years ago

我想问一个问题呢,我用transform: translateY(...)来做动画的时候,元素并没有提升到合成层,但是却没有触发layout和paint。比较费解

<script>
        var flag = false
        setInterval(function () {
            flag = !flag
            target.style.transform = flag ? 'translateY(10px)' : 'translateY(-10px)';
        },1000)
    </script>
<body>
<div id="target">
        某个元素
    </div>
</body>
greatbear412 commented 3 years ago

我想问一个问题呢,我用transform: translateY(...)来做动画的时候,元素并没有提升到合成层,但是却没有触发layout和paint。比较费解

<script>
        var flag = false
        setInterval(function () {
            flag = !flag
            target.style.transform = flag ? 'translateY(10px)' : 'translateY(-10px)';
        },1000)
    </script>
<body>
<div id="target">
        某个元素
    </div>
</body>

image 估计你是Chrome吧?Chrome的Blink引擎下,transform不会触发Layout和Paint