重绘、重排、合成
三个和渲染流水线相关的概念——“重排”“重绘”和“合成”。理解了这三个概念对于你后续 Web 的性能优化会有很大帮助。
更新了元素的几何属性(重排)
从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,当我们改变一个元素的尺寸位置属性时,会重新进行样式计算(Computed Style),布局(Layout),绘制(Paint)以及后面的所有流程,这种行为我们称为重排无疑,重排需要更新完整的渲染流水线,所以资源开销也是最大的。
更新元素的绘制属性(重绘)
接下来,我们再来看看重绘,比如通过 JavaScript 更改某些元素的背景颜色,渲染流水线会怎样调整呢?
从图中可以看出,当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制,这个就是重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
直接合成阶段
那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。具体流程参考下图:
在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
总结:
重排和重绘都会占用主线程。还有一个东西也在主线程上运行,就是 JS,如果他们都在主线程上,就会出现抢占执行时间的问题,如果你写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式计算布局和绘制的操作。
当页面以每秒 60 帧的刷新率时才不会让用户感觉到页面卡顿,如果你再运行动画时还有大量的 JS 任务需要执行,因为布局,绘制和 JS 的执行都是在主线程上执行。当在一帧的事件内布局和绘制结束后,如果还有剩余时间,JS 就会拿到主线程的使用权,如果 JS 执行时间过长,就会导致在下一帧开始 JS 没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面动画的卡顿。
优化手段是什么?
第一种是通过 requestAnimationFrame()这个方法会在每一帧被调用,通过 API 的回调,然后我们可以把 JS 的运行任务分成一些更小的任务块(分到每一帧),在每一帧时间用完前暂停 JS 执行,归还主线程,这样的话在下一帧开始的时候,主线程就可以按时进行布局和绘制。React 最新的引擎 React Fiber 就是用到了这个 api 来做了很多优化。
第二种方法,我们可以知道栅格化的整个流程是不占用主线程的,只在合成器线程和栅格线程中运行,这意味着它不需要和 JS 抢夺主线程,不断的重绘和重排可能会导致掉帧,是因为 JS 执行阻塞了主线程,而 CSS 中有个动画属性叫 Trasform,通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程,所以不会受到主线程中 JS 的影响,更重要的是通过 transform 实现的动画由于不需要经过布局绘制,样式计算等操作,所以节省了很多运算时间(方便实现负责的动画),我们常常会使用哪些手续来实现动画效果呢?位置变化,宽高变化(旋转,3D 等),这些都是可以使用 transform 来代替的。