浏览器之分层和合成机制CSS动画比JavaScript更高效

显示器是怎么显示图像的

每个显示器都有固定的刷新频率,通常是60HZ,也就是每秒更新60张图片,更新的图片都来自于显卡中一个叫“前缓冲区”的地方,显示器做的任务很简单,就是每秒固定读取60次前缓冲区中的图像,并将读取的图像显示到显示器上。

那么这里显卡是做什么的呢?

显卡的职责是和诚信的图像,并将图像保存到“后缓冲区”中。一旦显卡把合成的图像写道后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显示器可以读取到最新的显卡课程的图像。

一般更新频率和显示器的刷新频率是一致的,但是偶时候在一些复杂场景中,显卡处理一张图片的速度会变慢,这样就会造成视觉上的卡顿。

帧VS频率

理解显示器的原理后,明确下 帧和频率的概念。

当你滚动条滚动页面,或者手指缩放页面时屏幕就会产生动画效果,之所以你能感受到有动画效果,因为滚动或者缩放操作的时候,渲染引擎会通过渲染流水线生成新的图片,并且发送到显卡的后缓冲区。

大多数设备的更新频率是60HZ, 也就是要在屏幕上产生流畅的动画需要每秒更新60张图片到缓冲区。

我们把渲染流水线生成的每一幅图片称为一帧,把渲染流水线每秒更新了多少帧称为帧率。 比如滚动过程中1秒更新了60帧,就说帧率是60HZ或者60FPS。

由于用户很容易观察到那些丢失的帧,如果在一个动画过程中,渲染引擎生成某些帧的时间过久,用户就会感觉到卡顿。

解决卡顿问题,就要解决每帧生成过久的问题。Chrome做了大量的工作,其中最卓有成效的策略就是引入分层和合成机制。

分成和合成机制代表当今最先进的渲染技术。下面就来分析分析。

如何生成一帧图像

回顾下 -> 浏览器渲染流程 介绍的渲染流水线。

关于其中任意一帧的生成方式,有重排(Reflow)、重绘(Repaint)和合成(Composite)三种方式。

这三种方式的路径是不同的,通常渲染的路径长,生成图像花费的时间就越多。比如重排,他需要根据CSSMOM和DOM来计算布局树,这样生产一幅图片会让整个渲染流水线的每个阶段都执行一遍,如果布局复杂的化就很难保证渲染效率。

重绘的效率稍微高一点。合成的操作路径就非常短。如果采用了GPU,合成效率会非常高。

下面深入分析Chrome浏览器的合成技术。

分层和合成

通常页面的组成是非常复杂的,有的页面要实现一些复杂的动画效果,比如点击菜单的动画效果。滚动鼠标的当年规划效果,炫酷的3D效果。如果没有采用分层机制,直接从布局树生成目标图片,那么每次页面有很小的变化都会触发重排或者重绘机制。这种牵一发动全身的绘制策略会严重印象渲染效率。

为了提高渲染效率,Chrome引如分层和合成机制。如何理解他们?

可以把一张网页想象成由很多图片叠加在一起,每个图片就对应一个图层,Chrome合成器最终将这些图层合成了用于显示页面的图片。

考虑到一个页面被划分为两个层,当进行下一帧的渲染时,上一帧可能需要实现某些变换,如平移、旋转、缩放、阴影或者Alpha渐变,这时候合成器只需要将两层进行相应的变化操作就可以了。显卡处理器这些操作驾轻就熟,所以这个过程时间非常短。

再来看看Chrome如何实现这个机制。

Chrome在渲染流水线中,分层体现在生成分部树之后,渲染引擎会根据布局树的特点将其转换为层树(Layer Tree) 层数是渲染流水线后续流程的基础结构。

参考 -> 浏览器渲染流程 中介绍的,绘制阶段是产生一个绘制指令组合成一个列表。

有了绘制列表之后,就需要进入光栅化阶段,光栅化就是按照绘制列表中的指令生成图片。没一个图层都对应一张图片。合成线程有了这些图片之后,会将这些图片合成一张图片,最后将结果发送到后缓冲区。这是一个大致的分层、合成流程。

需要关注的是合成操作是在合成线程上完成的,这意味着执行合成操作的时候,不会影响主线程执行。这就是为什么主线程可能卡住了,而CSS动画依然可以执行的原因。

分块

如果分层是从宏观层面上提高了渲染效率。那么分块是从微观层面上提高了渲染效率。

合成线程会把每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块。这样就可以大大的加速页面的显示速度。不过有时候,即使优先级最高的图块也会浪费不少时间。这就涉及到一个关键因素——纹理上传。这是因为从计算机内存上传到GPU内存的操作会比较慢。

为了解决这个问题,Chrome采取一个策略,在首次合成图块的时候使用一个低分辨率图片。比如正常分辨率一半,这样纹理就减少了四分之三,首次显示的时候,先将低分辨率图片展示出来,然后继续合成正常比例的页面内容,当正常的页面内容绘制完成后再替换掉低分辨率内容。这种方式虽然刚开始用户是看到低分辨率内容,但是也比刚开始什么都看不到要好。

利用底层分化技术优化代码

理解上面的原理,可以利用这个来优化代码。

再web应用的时候,你可能经常要对一些元素进行变换操作,这时候使用JS来写会牵扯到整个流水线操作,所以JS绘制效率会非常地下。

这时候你可以使用 will-change 来告诉渲染引擎你会对高元素做一些特效变换。

.box {
    will-change: transform, opacity;
}

z这段代码就是提前告诉渲染引擎box要做几何变换和透明度变换操作。这时候渲染引擎会该元素单独实现一层。

但是凡是都有两面,滥用 will-change会制造过多的层,增加内存开销。

Mark24

Everything can Mix.