分层和合成机制:为什么CSS动画比JavaScript高效
1. 显示器是怎么显示图像的
每个显示器都有固定的刷新频率,通常是 60HZ,也就是每秒更新 60 张图片。这些更新的图片都来自于显卡中一个叫前缓冲区的地方。显示器所做的任务很简单,就是每秒固定读取 60 次前缓冲区中的图像,并将读取的图像显示到显示器上。
当显卡生成新的图像时,它会将这些图像写入前缓冲区。然后,显示器在每次刷新时,都会从前缓冲区读取最新的图像数据。这样,用户就可以看到连续流畅的动画效果,而不是静态的图像。
然而,如果显卡的渲染速度与显示器的刷新率不同步,就可能出现图像撕裂的问题。为了解决这个问题,现代显卡通常会使用双缓冲技术。在这种技术中,显卡会在一个后缓冲区中生成新的图像,然后在显示器刷新时,快速将后缓冲区的内容复制到前缓冲区。这样,显示器总是从前缓冲区读取完整的图像,从而避免了图像撕裂的问题。
2. 显卡如何生成图像数据?
显卡的主要职责是生成或合成新的图像。这个过程涉及到复杂的计算,包括顶点处理、光栅化和像素处理。一旦显卡生成了新的图像,它会将这些图像数据写入后缓冲区。
在双缓冲技术中,显卡会使用两个缓冲区:前缓冲区和后缓冲区。前缓冲区包含当前显示在显示器上的图像,而后缓冲区包含显卡正在生成的新图像。当显卡完成新图像的生成后,系统会让后缓冲区和前缓冲区互换。这样,显示器在下一次刷新时,就可以从前缓冲区读取新的图像。
通常情况下,显卡的更新频率和显示器的刷新频率是一致的,以保证图像的流畅显示。然而,在一些复杂的场景中,显卡处理一张图片的速度可能会变慢,这就可能导致视觉上的卡顿。为了解决这个问题,一些高级的显卡和显示器支持变动刷新率(如NVIDIA的G-Sync和AMD的FreeSync),可以动态调整显示器的刷新率以匹配显卡的输出速度,从而提供更流畅的视觉体验。
3. 如何生成一帧图像
在浏览器的渲染流程中,生成一帧图像的方式主要有三种:重排、重绘和合成。
重排是指当 DOM 结构中的元素发生改变,需要重新计算布局树,这会触发整个渲染流水线的每个阶段。这种方式的效率较低,因为它需要重新计算布局,并且如果布局复杂,那么渲染的效率会更低。 它需要重新根据 CSSOM 和 DOM 来计算布局树,这样生成一幅图片时,会让整个渲染流水线的每个阶段都执行一遍,如果布局复杂的话,就很难保证渲染的效率了。
重绘是指当元素的外观发生改变,但布局没有改变时,浏览器会重新绘制这些元素。这种方式的效率比重排稍高,因为它不需要重新布局,但仍然需要重新计算绘制信息,并触发绘制操作之后的一系列操作。 重绘因为没有了重新布局的阶段,操作效率稍微高点,但是依然需要重新计算绘制信息,并触发绘制操作之后的一系列操作。
合成则是最高效的方式。它不需要重新布局和绘制,只需要在 GPU 上进行合成操作。因此,如果可能,我们推荐优先使用合成方式生成图像。如果合成方式不能满足需求,那么可以考虑使用重绘或重排的方式。 相较于重排和重绘,合成操作的路径就显得非常短了,并不需要触发布局和绘制两个阶段,如果采用了 GPU,那么合成的效率会非常高。
在 Chrome 浏览器中,合成技术主要包括三个步骤:分层、分块和合成。分层是指将页面分解为多个独立的层,每个层可以独立进行合成操作。分块是指将每个层进一步分解为多个块,每个块可以独立进行合成操作。最后,浏览器会将所有的块合成为一幅完整的图像。
3.1 重排
重排,也被称为布局或回流,是当 DOM 的变化影响到元素的几何属性(例如宽度、高度、位置等)时,浏览器需要重新计算元素的位置和大小。这是一个相当昂贵的操作,因为它可能涉及到整个渲染树的更新。例如,当你添加或删除 DOM 节点,或者当你应用新的 CSS 样式时,都可能触发重排。
3.2 重绘
重绘是当元素的外观发生改变,但没有改变布局时,浏览器需要重新绘制这些元素。例如,改变元素的颜色、背景色、边框颜色等都会触发重绘。重绘不如重排那么昂贵,但是如果在短时间内连续发生大量的重绘,仍然可能导致性能问题。
3.3 合成
Chrome 为了提升每帧的渲染效率,引入了分层和合成的机制。这个机制可以类比于 PhotoShop 中的图层概念。在 PhotoShop 中,一个项目由多个图层构成,每个图层可以是一张单独的图片,可以设置透明度、边框阴影,可以旋转或者设置图层的上下位置。将这些图层叠加在一起后,就能呈现出最终的图片。
同样,你可以将一个网页视为由多个图层叠加在一起的复合图像。每个图层对应网页的一部分,Chrome 的合成器将这些图层合成为最终用于显示的页面图像。在这个过程中,将网页元素分解为多个图层的操作称为分层,最后将这些图层合并到一起的操作称为合成。分层和合成通常是一起使用的。
当渲染到下一帧时,如果某个图层需要实现某些变换,如平移、旋转、缩放、阴影或者 Alpha 渐变,合成器只需要对该图层进行相应的变化操作,而无需影响其他图层。由于显卡对这些操作处理得非常高效,所以合成过程的时间非常短。
合成是最高效的渲染方式,它避免了昂贵的布局和绘制过程。在合成阶段,浏览器会生成位图,然后直接在 GPU 上进行合成,这大大提高了渲染的效率。在 Chrome 浏览器中,一些特定的 CSS 属性(如 transform 和 opacity)会触发合成。此外,通过使用 will-change
属性,开发者可以提前告诉浏览器哪些元素可能会发生变化,这样浏览器可以提前进行优化。
Chrome 中的合成技术,可以用三个词来概括总结:分层、分块和合成。分层是将页面元素分解为多个图层,分块是将每个图层进一步分解为多个块,合成则是将所有的块合成为一幅完整的图像。
通常页面的组成是非常复杂的,有的页面里要实现一些复杂的动画效果,比如点击菜单时弹出菜单的动画效果,滚动鼠标滚轮时页面滚动的动画效果,当然还有一些炫酷的 3D 动画效果。如果没有采用分层机制,从布局树直接生成目标图片的话,那么每次页面有很小的变化时,都会触发重排或者重绘机制,这种“牵一发而动全身”的绘制策略会严重影响页面的渲染效率。
理解了为什么要引入合成和分层机制后,我们再来看看 Chrome 是如何实现分层和合成机制的。
3.3.1 分层
分层是指将页面的各个元素分解为独立的层。每个层都可以独立进行合成操作。这样做的好处是,当页面的某一部分发生变化时,只需要重新合成那一部分的层,而不需要重新合成整个页面。这大大提高了渲染的效率。在 Chrome 中,一些特定的 CSS 属性(如 transform 和 opacity)会触发新的层的创建。
在 Chrome 的渲染流水线中,分层体现在生成布局树之后,渲染引擎会根据布局树的特点将其转换为层树(Layer Tree),层树是渲染流水线后续流程的基础结构。
层树中的每个节点都对应着一个图层,下一步的绘制阶段就依赖于层树中的节点。绘制阶段其实并不是真正地绘出图片,而是将绘制指令组合成一个列表,比如一个图层要设置的背景为黑色,并且还要在中间画一个圆形,那么绘制过程会生成 | Paint BackGroundColor: Black | Paint Circle | 这样的绘制指令列表,绘制过程就完成了。 |
有了绘制列表之后,就需要进入光栅化阶段了,光栅化就是按照绘制列表中的指令生成图片。每一个图层都对应一张图片,合成线程有了这些图片之后,会将这些图片合成为“一张”图片,并最终将生成的图片发送到后缓冲区。这就是一个大致的分层、合成流程。
需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。这就是为什么经常主线程卡住了,但是 CSS 动画依然能执行的原因。
3.3.2 分块
分块是指将每个层进一步分解为多个块。每个块都可以独立进行合成操作。这样做的好处是,当层的某一部分发生变化时,只需要重新合成那一部分的块,而不需要重新合成整个层。这进一步提高了渲染的效率。在 Chrome 中,块的大小通常是 256x256 或 512x512 像素。
如果说分层是从宏观上提升了渲染效率,那么分块则是从微观层面提升了渲染效率。
通常情况下,页面的内容都要比屏幕大得多,显示一个页面时,如果等待所有的图层都生成完毕,再进行合成的话,会产生一些不必要的开销,也会让合成图片的时间变得更久。
因此,合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。不过有时候,即使只绘制那些优先级最高的图块,也要耗费不少的时间,因为涉及到一个很关键的因素——纹理上传,这是因为从计算机内存上传到 GPU 内存的操作会比较慢。
为了解决这个问题,Chrome 又采取了一个策略:在首次合成图块的时候使用一个低分辨率的图片。比如可以是正常分辨率的一半,分辨率减少一半,纹理就减少了四分之三。在首次显示页面内容的时候,将这个低分辨率的图片显示出来,然后合成器继续绘制正常比例的网页内容,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。这种方式尽管会让用户在开始时看到的是低分辨率的内容,但是也比用户在开始时什么都看不到要好。
3.3.3 合成
合成是指将所有的块合成为一幅完整的图像。这个过程在 GPU 上进行,因此非常高效。在合成阶段,浏览器会应用各种视觉效果,如 CSS 过滤器和混合模式。最后,合成的结果会被写入前缓冲区,然后在下一次屏幕刷新时,显示到屏幕上。
98. conclusion
- 首先我们介绍了显示器、显卡基本工作原理。
- 介绍渲染引擎生成一帧图像有三种方式:重排、重绘和合成。
- 其中重排和重绘操作都是在渲染进程的主线程上执行的,比较耗时;
- 而合成操作是在渲染进程的合成线程上执行的,执行速度快,且不占用主线程。
- 然后介绍了浏览器是怎么实现合成的,其技术细节主要可以使用三个词来概括:
- 分层
- 分块
- 合成。
99. quiz
1. 如果显卡生图像速度小于或者大于显示器刷新频率会怎么样
如果显卡生成图像的速度小于显示器的刷新频率,那么显示器在刷新时可能没有新的图像可以显示。这种情况下,显示器可能会显示前一帧的图像,导致动画效果不流畅,用户可能会感觉到卡顿。
如果显卡生成图像的速度大于显示器的刷新频率,那么可能会出现图像撕裂的问题。这是因为在显示器还在显示一帧图像的时候,显卡已经生成了新的图像并写入前缓冲区,导致显示器显示的图像是两帧图像的混合,上半部分是新的图像,下半部分是旧的图像。
为了解决这些问题,现代显卡通常会使用双缓冲技术。在这种技术中,显卡会在一个后缓冲区中生成新的图像,然后在显示器刷新时,快速将后缓冲区的内容复制到前缓冲区。这样,显示器总是从前缓冲区读取完整的图像,从而避免了图像撕裂的问题。
此外,一些高级的显卡和显示器支持变动刷新率(如NVIDIA的G-Sync和AMD的FreeSync),可以动态调整显示器的刷新率以匹配显卡的输出速度,从而提供更流畅的视觉体验。
2. 如何利用分层技术优化代码
通过上面的介绍,相信你已经理解了渲染引擎是怎么将布局树转换为漂亮图片的,理解其中原理之后,你就可以利用分层和合成技术来优化代码了。
在写 Web 应用的时候,你可能经常需要对某个元素做几何形状变换、透明度变换或者一些缩放操作,如果使用 JavaScript 来写这些效果,会牵涉到整个渲染流水线,所以 JavaScript 的绘制效率会非常低下。
这时你可以使用 will-change 来告诉渲染引擎你会对该元素做一些特效变换,CSS 代码如下:
.box {
will-change: transform, opacity;
}
这段代码就是提前告诉渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变帧,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。这也是 CSS 动画比 JavaScript 动画高效的原因。
所以,如果涉及到一些可以使用合成线程来处理 CSS 特性或者动画的情况,就尽量使用 will-change 来提前告诉渲染引擎,让它为该元素准备独立的层。但是凡事都有两面性,每当渲染引擎为一个元素准备一个独立层的时候,它占用的内存也会大大增加,因为从层树开始,后续每个阶段都会多一个层结构,这些都需要额外的内存,所以你需要恰当地使用 will-change。