Chrome浏览器的分层合成机制

# Chrome浏览器的分层合成机制

# 显示器显示图像

每个显示器都有固定的刷新率,比如60HZ就是指每秒显示60张图片。一张图片就是一帧,60Hz就是帧率。

对于浏览器而言,生成图片是渲染引擎的工作,渲染引擎会将生成的图片发送到显卡的后缓冲区,显示器读取显卡中的图片进行显示。

如果渲染引擎生成一帧的时间变久,那么用户就会感觉到页面卡顿,因此解决卡顿问题就是要解决帧生成慢的问题。

渲染引擎是如何生成一帧图像的呢?有三种方式:重排、重绘、合成。

每种方式对应的渲染路径不同,渲染路径越长,那么生成图像所花费的时间就越长。所谓渲染路径就是渲染引擎从接收html文件到绘制显示页面过程中的一系列重要节点,大致包括了html解析生成DOM、CSS解析生成CSSOM、DOM与CSSOM合成构建渲染树,根据渲染树进行布局(Layout)、绘制页面(paint)。

重排是由于页面元素的大小或位置发生改变,需要重新计算DOM和CSSOM生成渲染树,重新布局和绘制,因此所需的时间是三者中最长的。

重绘一般是由于页面元素的样式发生了变化,但是布局没变,所以不需要重新进行布局操作,效率稍微高一些,但是也要重新计算样式信息。

合成则不需要布局和绘制两个阶段,渲染路径是最短的,如果使用了GPU会更快。

所以Chrome引入了分层和合成机制,Chrome的合成概括为三个词:分层、分块和合成。

# 分层与合成

玩过PS的都知道,一张图像可以分为很多个图层,所有的图层放在一起就得到了最终的图像。我们可以对图层进行单独的操作。

而Chrome浏览器就使用了分层的策略。为什么呢?想像以下图像只有一层,那么每次元素的小变动都有可能引发重排和重绘,牵一发而动全身,严重影响页面的渲染效率。如果进行了分层,其中一层的元素变化了,比如旋转平移阴影等,合成器只需要对该层进行相应操作之后把图层合成在一起就好了,显卡处理这些操作驾轻就熟,所以合成时间会非常短。

# Chrome如何实现分层与合成?

分层是在生成布局树之后,渲染引擎根据其特点生成层树(Layer Tree)。

生成树的每个节点就对应一个图层。

绘制阶段的工作并不是绘出图片,而是生成绘制指令列表,然后进入光栅化阶段,根据绘制列表中的指令生成图片,每一层对应一个图片,合成线程再将图层合成一张图片并送入显卡后缓冲区。需要注意合成是在合成线程上完成的,不影响主线程执行,这就是为什么有时候主线程卡住了,css动画还能执行的原因。

# 分块

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

合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。

不过有时候, 即使只绘制那些优先级最高的图块,也要耗费不少的时间,为了解决这个问题,Chrome 又采取了一个策略:在首次合成图块的时候使用一个低分辨率的图片,然后合成器继续绘制正常比例的网页内容,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。

# 利用分层技术优化代码

说了那么多原理,其实就是为了能够在实践中利用。

在写 Web 应用的时候,你可能经常需要对某个元素做几何形状变换、透明度变换或者一些缩放操作,如果使用 JavaScript 来写这些效果,会牵涉到整个渲染流水线,所以 JavaScript 的绘制效率会非常低下。

恰当使用will-change

可以使用 will-change 来告诉渲染引擎你会对该元素做一些特效变换,CSS 代码如下:

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

这段代码就是提前告诉渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。这也是 CSS 动画比 JavaScript 动画高效的原因。

但是凡事都有两面性,每当渲染引擎为一个元素准备一个独立层的时候,它占用的内存也会大大增加,因为从层树开始,后续每个阶段都会多一个层结构,这些都需要额外的内存,所以你需要恰当地使用 will-change。