​ 每个显示器都有固定的刷新频率,通常是60HZ(即每秒刷新60次),更新的图片都来自于显卡中前缓冲区。显卡会合成新的图像,并将其保存到后缓冲区,一旦新合成的图像写到后缓冲区,系统就会让后缓冲区前缓冲区交换,以此保证显示器读取到新合成的图像。在浏览器中,渲染引擎会通过渲染流水线生成新的图片,并发送到显卡的后缓冲区。由于大多数屏幕的更新频率都是60次/秒,这就意味着渲染引擎每秒需要更新60张图片到显卡的后缓存区,一旦渲染引擎生成某些帧(一张图片就是一帧)的时间过久,用户就会感到卡顿。Chrome浏览器引入了分层合成机制,以此来解决卡顿或每帧生成时间过久的问题。

如何生成一帧图像

浏览器渲染流水线中任意一帧生成的方式有:重排重绘合成三种。三种方式生成一帧图像的路径是不同的,但通常路径越长,生成图像说花费的时间就越多。

重排:需要重新根据CSSOM和DOM来计算布局树,这样生成一幅图片时,会让整个渲染流水线的每个阶段都执行一遍。

重绘:需要重新计算绘制信息,并触发绘制操作之后的一系列操作。

合成:不需要触发布局和绘制两个阶段,如果采用了GPU,那么合成效率更高。

所以,渲染引擎生成一帧图像的几种方式按照效率会优先使用合成,若不能满足需求,那么再使用重排或者重绘;

分层与合成

​ 通常页面的组成是非常复杂的,如果没有采用分层机制,从布局树直接生成目标图片的话一旦页面有很小的变化都会触发重排或者重绘,这种绘制策略会严重影响页面的渲染效率。为了提升每帧的渲染效率,我们可以将一张网页想象成多个图片叠加在一起的,每张图片对应一个图层,比如透明度、边框影阴、是否可旋转等,最后将这些图层叠加在一起后呈现最终的图片。将素材分解为多个图层的操作就称为分层,最后将这些图层合并到一起的操作就称为合成

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

分层与合成

​ 在Chrome的渲染流水线中,分层体现在生成布局树之后,渲染引擎会根据布局树的特点将其转换为层树(Layer Tree)。层树的每一个节点都对应着一个图层,下一步的绘制就依赖于层树中的节点。绘制阶段其实并不是真正地绘出图片,而是将绘制指令组合成一个列表。有了绘制列表之后,光栅化就是按照绘制列表中的指令生成图片,每一个图层对应一张图片,合成线程有了这些图片之后,会将这些图片合成为“一张”图片,并最终将生成的图片发送到后缓冲区。合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的

分块

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

​ 通常情况下,页面的内容要比屏幕大的多,如果想要显示一个页面要等所有的图层都生成完了再进行合成的话会产生一些不必要的开销或者图片合成的时间变得更久。因此,合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,但是有时候即使优先绘制优先级高的图块也会因为纹理上传的原因耗费不少时间。

​ 为此,Chrome 又采取了一个策略:在首次合成图块的时候使用一个低分辨率的图片。比如可以是正常分辨率的一半,分辨率减少一半,纹理就减少了四分之三。在首次显示页面内容的时候,将这个低分辨率的图片显示出来,然后合成器继续绘制正常比例的网页内容,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。

使用分层技术优化代码

​ 使用will-change来告诉渲染引擎对该元素做一些特效变换。当我们需要对某个元素做几何形状变换、透明度变换或者一些缩放操作,如果使用 JavaScript 来写这些效果,会牵涉到整个渲染流水线,所以 JavaScript 的绘制效率会非常低下。

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

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

参考文献:《浏览器工作原理与实践》-李兵