文档结构、元素的样式、元素的几何关系、绘画顺序,这些信息我们都有了,这个时候如果要绘制一个页面,我们需要做的是 把这些信息转化为显示器中的像素,这个转化的过程,叫做 光栅化(rasterizing)。
那我们要绘制一个页面,最简单的做法是只光栅化视口内(viewport)的网页内容,如果用户进行了页面滚动,就移动光栅帧(rastered frame)并且光栅化更多的内容以补上页面缺失的部分,如下:
Chrome 第一个版本就是采用这种简单的绘制方式,这一方式唯一的缺点就是每当页面滚动,光栅线程都需要对新移进视图的内容进行光栅化,这是一定的性能损耗,为了优化这种情况,Chrome 采取一种更加复杂的叫做合成(compositing)的做法。
那么,什么是合成?
合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。当用户滚动页面时,由于页面各个层都已经被光栅化了,浏览器需要做的只是合成一个新的帧来展示滚动后的效果罢了。页面的动画效果实现也是类似,将页面上的层进行移动并构建出一个新的帧即可。
为了实现合成技术,我们需要对元素进行分层,确定哪些元素需要放置在哪一层,主线程需要遍历渲染树来创建一棵层次树(Layer Tree),对于添加了 will-change
CSS 属性的元素,会被看做单独的一层,没有 will-change
CSS 属性的元素,浏览器会根据情况决定是否要把该元素放在单独的层。
你可能会想要给页面上所有的元素一个单独的层,然而当页面的层超过一定的数量后,层的合成操作要比在每个帧中光栅化页面的一小部分还要慢,因此衡量你应用的渲染性能是十分重要的一件事情。
一旦 Layer Tree 被创建,渲染顺序被确定,主线程会把这些信息通知给合成器线程,合成器线程开始对层次数的每一层进行光栅化。有的层的可以达到整个页面的大小,所以合成线程需要将它们切分为一块又一块的小图块(Tiles),之后将这些小图块分别进行发送给一系列光栅线程(raster threads)进行光栅化,结束后光栅线程会将每个图块的光栅结果存在 GPU Process 的内存中。
为了优化显示体验,合成线程可以给不同的光栅线程赋予不同的优先级,将那些在视口中的或者视口附近的层先被光栅化。
当图层上面的图块都被栅格化后,合成线程会收集图块上面叫做绘画四边形(draw quads)的信息来构建一个合成帧(compositor frame)。
以上所有步骤完成后,合成线程就会通过 IPC 向浏览器进程(browser process)提交(commit)一个渲染帧。这个时候可能有另外一个合成帧被浏览器进程的 UI 线程(UI thread)提交以改变浏览器的 UI。这些合成帧都会被发送给 GPU 从而展示在屏幕上。如果合成线程收到页面滚动的事件,合成线程会构建另外一个合成帧发送给 GPU 来更新页面。
合成的好处在于这个过程没有涉及到主线程,所以合成线程不需要等待样式的计算以及 JavaScript 完成执行。这就是为什么合成器相关的动画最流畅,如果某个动画涉及到布局或者绘制的调整,就会涉及到主线程的重新计算,自然会慢很多。
浏览器接收到网站页面文档后,会将文档中的标记语言解析为 DOM 树。DOM 树和 CSS 结合形成浏览器构建页面的渲染树。渲染树中包含了大量的渲染元素,每个渲染元素会被分到单独一个图层中,每个图层又会被加载到 GPU 形成渲染纹理,而图层在 GPU 中 transform
是不会触发 repaint
的,这点非常类似 3D 绘图功能,最终这些使用 transform
的图层都会由 独立的合成器进程进行处理。
CSS transform
创建新的复合图层,可以被 GPU 直接用来执行 transofmr
操作。在 Chrome 开发者工具中开启 Show Layer Border 选项后,每个复合涂层就会显示一条黄色的边界。
每个页面元素都有一个独立的渲染进程,包含了主线程和合成线程。
当我们通过某种方法引起浏览器的 reflow
时,需要重新经历样式计算(Style Calculation)和布局(Layout)阶段,导致浏览器重新计算页面中每个 DOM 元素的尺寸及重新布局,伴随着重新进行 repaint
,这个过程是非常耗时的。为了把代价降到最低,当然最好只留下合成(Composite)这个步骤最好。假设当我们改变一个容器的样式时,影响的只是它自己,并且还无需重绘,直接通过 GPU 中改变纹理的属性来改变样式,岂不是更好?
如果能让元素达到这个效果?
就是让 DOM 元素拥有自己的层(Layer)。有了层的概念,让我们从层的概念再来看浏览器的渲染过程:
可以将这个过程理解为设计师的 Phtoshop 文件。在 PS 源文件中,一个图像是由若干个图层相互叠加而展示出来的。分成多个图层的好处就是每个图层相对独立,修改方便,对单个图层的修改不会影响到页面上的其他图层。因此层(Layer)存在的意义在于:用最小的代价来改变某个页面元素。可以将某个 CSS 动画或某个 JS 交互效果抽离到一个单独的渲染层,来达到加速渲染的目的。
那么哪些规则能让浏览器主动帮我们创建独立的层呢?
perspective
和 transform
) 的 CSS 属性backgace-visibility
为 hidden
的元素<video>
元素<canvas>
元素opacity
、transform
、filter
、backdrop-filter
应用了 animation
或者 transition
(需要是 active
的 animation
或者 transition
,当 animation
或者 transition
效果未开始或结束后,合成层也会失效)will-change
设置为 opacity
、transform
、top
、left
、bottom
、right
(其中 top
、left
等需要设置明确的定位属性,如 relative
等)filter
过滤器的元素z-index
较小,那这个元素也会被应用到复合图层实际优化点:
translate
替代 top
改变opacity
替代 visibility
class
,然后修改 DOM 的 className
display: none
(有一次 Reflow 重排),然后你修改 100 次,然后再把它显示出来table
布局,可能很小的一个小改动会造成整个 table
的重新布局CSS 中以下几个属性能触发硬件加速:
transform
opacity
filter
will-change
如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用硬编码的技巧来诱导浏览器开启硬件加速。
.element {-webkit-transform: translateZ(0);-moz-transform: translateZ(0);-ms-transoform: translateZ(0);-o-transoform: translateZ(0);transform: translateZ(0);transform: rotateZ(360deg);transform: translate3d(0, 0, 0);}
这段代码的作用就是让浏览器执行 3D transform
。浏览器通过该样式创建了一个独立图层,图层中的动画则有 GPU 进行预处理并且触发了硬件加速。
如果某个元素的背后是一个复杂元素,那么该元素的 repaint
操作就会耗费大量的资源,此时也可以使用上面的技巧来减少性能开销。
注意事项: