以 60Hz 的刷新率的显示器为例,显示器每秒刷新 60 次,每次刷新都会从 前缓冲区 中读取一帧图像显示在屏幕上。显卡会绘制下一帧图像保存在 后缓冲区 中。当显示器完成一次刷新后,系统会让前缓冲区和后缓交换,这样就完成了一次显示。通常情况下,显卡的更新频率和显示器的刷新频率是一致的。在一些复杂的场景中,显卡处理一张图片的速度会变慢,这样就会造成视觉上的卡顿。
渲染引擎通过重排、重绘、合成等操作,最终会生成一帧图像。这一帧图像会被保存在后缓冲区中,等待显示器刷新时显示在屏幕上。
- 重排:元素的几何属性发生变化,需要重新计算布局树,然后进行重绘和合成。
- 重绘:元素的样式属性发生变化,需要重新计算样式,然后进行合成。
- 合成:将图层合成为一帧图像。
为了提升每帧的渲染效率,Chrome 引入了分层和合成的机制。
可以把一张网页想象成是由很多个图片叠加在一起的,每个图片就对应一个图层。将素材分解为多个图层的操作就称为分层、最后将这些图层合并到一起的操作就称为合成。
当某些元素发生变换,如平移、旋转、缩放 或者透视变换时,这些元素会被单独分成一个图层。这样就可以只对这个图层进行重绘和合成,而不用重新计算整个页面的布局和样式。
需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。 这就是为什么主线程卡住了,但是 CSS 动画依然能执行的原因。
合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,大大加速页面的显示速度。
由于从计算机内存上传到 GPU 内存的操作会比较慢。即使只绘制那些优先级最高的图块,也要耗费不少的时间,这涉及到一个很关键的因素——纹理上传
因此,Chrome 又采取了一个策略:在首次合成图块的时候使用一个低分辨率的图片。比如正常分辨率的一半,分辨率减少一半,纹理就减少了四分之三。 直到正常比例的纹理上传完成,再将低分辨率的纹理替换掉。
当我们使用 JavaScript 来实现动画时,通常是通过改变元素的样式属性来实现的。这样就会触发重排、重绘和合成操作。而当我们使用 CSS 来实现动画时,通常是通过改变元素的 transform 属性来实现的。这样就会触发合成操作。
/* theme.css */
div{
will-change: transform, opacity;
}
渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。这也是 CSS 动画比 JavaScript 动画高效的原因。。
使用 js 改变transform也能享受这个属性带来的优化。