https://juejin.im/entry/590801780ce46300617c89b8

渲染

这张很经典的图许多人都看过,其中的概念大家应该都很熟悉,也就是这么几个步骤:
js修改dom结构或样式 -> 计算style -> layout(重排) -> paint(重绘) -> composite(合成)

但是其中有更复杂的内容,我们从更底层来详细说明这个过程,主要是下面这两幅图:

上图出自GPU Accelerated Compositing in Chrome

上图出自The Anatomy of a Frame

这部分内容基于blink、webkit内核,但是其中涉及到的重排、重绘、composite和合成层提升等环节对于各大浏览器都是一致的。

先说一些概念纹理

  1. 纹理其实就是GPU中的位图,存储在GPU video RAM中。

  2. Rasterize(光栅化)

绘制的具体过程

我们先把计算样式、重排等步骤抽离,单独讲解浏览器是怎么绘制的。

先来看这幅经典的图:

图中一些名词的称呼发生了变化,详见taobaofed的文章:无线性能优化:Composite

Render Object

首先我们有DOM树,但是DOM树里面的DOM是供给JS/HTML/CSS用的,并不能直接拿过来在页面或者位图里绘制。因此浏览器内部实现了Render Object:

每个Render Object和DOM节点一一对应。Render Object上实现了将其对应的DOM节点绘制进位图的方法,负责绘制这个DOM节点的可见内容如背景、边框、文字内容等等。同时Render Object也是存放在一个树形结构中的。

既然实现了绘制每个DOM节点的方法,那是不是可以开辟一段位图空间,然后DFS遍历这个新的Render Object树然后执行每个Render Object的绘制方法就可以将DOM绘制进位图了?就像“盖章”一样,把每个Render Object的内容一个个的盖到纸上(类比于此时的位图)是不是就完成了绘制。

不,浏览器还有个层叠上下文。就是决定元素间相互覆盖关系(比如z-index)的东西。这使得文档流中位置靠前位置的元素有可能覆盖靠后的元素。上述DFS过程只能无脑让文档流靠后的元素覆盖前面元素。

因此,有了Render Layer。

Render Layer

当然Render Layer的出现并不是简单因为层叠上下文等,比如opacity小于1、比如存在mask等等需要先绘制好内容再对绘制出来的内容做一些统一处理的css效果。

总之就是有层叠、半透明等等情况的元素(具体哪些情况请参考无线性能优化:Composite)就会从Render Object提升为Render Layer。不提升为Render Layer的Render Object从属于其父级元素中最近的那个Render Layer。当然根元素HTML自己要提升为Render Layer。

因此现在Render Object树就变成了Render Layer树,每个Render Layer又包含了属于自己layer的Render Object。

另外:

The children of each RenderLayer are kept into two sorted lists both sorted in ascending order, the negZOrderList containing child layers with negative z-indices (and hence layers that go below the current layer) and the posZOrderList contain child layers with positive z-indices (layers that go above the current layer).
每个Render Layer的子Render Layer都是按照升序排列存储在两个有序列表当中的:negZOrderList存储了负z-indicices的子layers,posZOrderList存储了正z-indicies的子layers。
— 出自GPU加速的compositing一文

现在浏览器渲染引擎遍历 Layer 树,访问每一个 RenderLayer,然后递归遍历negZOrderList里的layer、自己的RenderObject、再递归遍历posZOrderList里的layer。就可以将一颗 Layer树绘制出来。

Layer 树决定了网页绘制的层次顺序,而从属于 RenderLayer 的 RenderObject 决定了这个 Layer 的内容,所有的 RenderLayer 和 RenderObject 一起就决定了网页在屏幕上最终呈现出来的内容。

层叠上下文、半透明、mask等等问题通过Render Layer解决了。那么现在:
开辟一个位图空间->不断的绘制Render Layer、覆盖掉较低的Layer->拿给GPU显示出来 是不是就完全ok了?

不。还有GraphicsLayers和Graphics Context

Graphics Layer(又称Compositing Layer)和Graphics Context

上面的过程可以搞定绘制过程。但是浏览器里面经常有动画、video、canvas、3d的css等东西。这意味着页面在有这些元素时,页面显示会经常变动,也就意味着位图会经常变动。每秒60帧的动效里,每次变动都重绘整个位图是很恐怖的性能开销。

因此浏览器为了优化这一过程。引出了Graphics Layers和Graphics Context,前者就是我们常说的合成层(Compositing Layer):

某些具有CSS3的3D transform的元素、在opacity、transform属性上具有动画的元素、硬件加速的canvas和video等等,这些元素在上一步会提升为Render Layer,而现在他们会提升为合成层Graphics Layer(你如果查看了前文我给的链接,你当时可能会疑惑为什么这些情况也能提升为Render Layer,现在你应该明白了,他们是为提升为Graphics Layer准备的)。每个Render Layer都属于他祖先中最近的那个Graphics Layer。当然根元素HTML自己要提升为Graphics Layer。

Render Layer提升为Graphics Layer的情况:

  • 3D 或透视变换(perspective、transform) CSS 属性
  • 使用加速视频解码的 元素
  • 拥有 3D (WebGL) 上下文或加速的 2D 上下文的 元素
  • 混合插件(如 Flash)
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
  • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
  • ….. 所有情况的详细列表参见淘宝fed文章:无线性能优化:Composite

3D transform、will-change设置为 opacity、transform等 以及 包含opacity、transform的CSS过渡和动画 这3个经常遇到的提升合成层的情况请重点记住。

另外除了上述直接导致Render Layer提升为Graphics Layer,还有下面这种因为B被提升,导致A也被隐式提升的情况,详见此文: GPU Animation: Doing It Right

每个合成层Graphics Layer 都拥有一个 Graphics Context,Graphics Context 会为该Layer开辟一段位图,也就意味着每个Graphics Layer都拥有一个位图。Graphics Layer负责将自己的Render Layer及其子代所包含的Render Object绘制到位图里。然后将位图作为纹理交给GPU。所以现在GPU收到了HTML元素的Graphics Layer的纹理,也可能还收到某些因为有3d transform之类属性而提升为Graphics Layer的元素的纹理。

现在GPU需要对多层纹理进行合成(composite),同时GPU在纹理合成时对于每一层纹理都可以指定不同的合成参数,从而实现对纹理进行transform、mask、opacity等等操作之后再合成,而且GPU对于这个过程是底层硬件加速的,性能很好。最终,纹理合成为一幅内容最终draw到屏幕上。

所以在元素存在transform、opacity等属性的css animation或者css transition时,动画处理会很高效,这些属性在动画中不需要重绘,只需要重新合成即可。

上述分层后合并的过程可以用一张图来描述:

绘制的具体实现

系统结构

进程

blink和webkit引擎内部都是使用了两个进程来搞定JS执行、页面渲染之类的核心任务。

  • Renderer进程
    主要的那个进程,每个tab一个。负责执行JS和页面渲染。包含3个线程:Compositor Thread、Tile Worker、Main thread,后文会介绍这三个线程。
  • GPU进程
    整个浏览器共用一个。主要是负责把Renderer进程中绘制好的tile位图作为纹理上传至GPU,并调用GPU的相关方法把纹理draw到屏幕上(一般的介绍浏览器渲染引擎的文章里都用paint这个词表述把内容光栅化和绘制到位图里,而用draw这个词表示GPU最终把纹理显示到屏幕上),所以这个CPU里的进程更应该称为“负责跟GPU打交道的进程”,不要像我之前一样因为不懂GPU以为是GPU里的一个进程, mdzz。GPU进程里只有一个线程:GPU Thread。

Renderer进程的三个线程

  • Compositor Thread
    这个线程既负责接收浏览器传来的垂直同步信号(Vsync,水平同步表示画出一行屏幕线,垂直同步就表示从屏幕顶部到底部的绘制已经完成,指示着前一帧的结束,和新一帧的开始), 也负责接收OS传来的用户交互,比如滚动、输入、点击、鼠标移动等等。
    如果可能,Compositor Thread会直接负责处理这些输入,然后转换为对layer的位移和处理,并将新的帧直接commit到GPU Thread,从而直接输出新的页面。否则,比如你在滚动、输入事件等等上注册了回调,又或者当前页面中有动画等情况,那么这个时候Compositor Thread便会唤醒Main Thread,让后者去执行JS、完成重绘、重排等过程,产出新的纹理,然后Compositor Thread再进行相关纹理的commit至GPU Thread,完成输出。
  • Main Thread

    这里大家就很熟悉了,chrome devtools的Timeline里Main那一栏显示的内容就是Main Thread完成的相关任务:某段JS的执行、Recalculate Style、Update Layer Tree、Paint、Composite Layers等等。
  • Compositor Tile Worker(s)
    可能有一个或多个线程,比如PC端的chrome是2个或4个,安卓和safari为1个或2个不等。是由Compositor Thread创建的,专门用来处理tile的Rasterization(前文说过的光栅化)。

可以看到Compositor Thread是一个很核心的东西,后面的俩线程都是由他主要进行控制的。
同时,用户输入是直接进入Compositor Thread的,一方面在那些不需要执行JS或者没有CSS动画、不重绘等的场景时,可以直接对用户输入进行处理和响应,而Main Thread是有很复杂的任务流程的。这使得浏览器可以快速响应用户的滚动、打字等等输入,完全不用进主线程。这里也有一个非常重要的点,后文会说。
再者,即使你注册了UI交互的回调,进了主线程,或者主线程很卡,但是因为Compositor Thread在他外面拦着,所以Compositor Thread依然可以直接负责将下一帧输出到页面上,因此即使你的主线程可能执行着高耗任务,超过16ms,但是你在滚动页面时浏览器还是能做出响应的(同步AJAX等特殊任务除外),所以比如你有一个比较卡的动画(动画的预先计算过程或者重绘过程超过16ms每帧),但是你滚动页面是非常流畅的,也就是动画卡而滚动不卡(随便给你个demo自己试试看)。

具体流程

一般我们在devtools的Timeline里大概会看到如下过程:

也就是JS执行后触发重绘重排等操作。这里着重分析背后的运行过程,即下面这副图:

图里后半部分有两处commit,分别是主线程通知Main Thread可以执行光栅化了,以及光栅化完成、纹理生成完毕,Compositor Thread通知GPU Thread可以将纹理按照指定的参数draw到屏幕上。

整体流程:

  1. Vsync
    接收到Vsync信号,这一帧开始
  2. Input event handlers
    之前Compositor Thread接收到的用户UI交互输入在这一刻会被传入给主线程,触发相关event的回调。

    All input event handlers (touchmove, scroll, click) should fire first, once per frame, but that’s not necessarily the case; a scheduler makes best-effort attempts, the success of which varies between Operating Systems.

    这意味着,尽管Compositor Thread能在16ms内接收到OS传来的多次输入,但是触发相应事件、传入到主线程被JS感知却是每帧一次,甚至可能低于每帧一次。也就是说touchmove、mousemove等事件最快也就每帧执行一次,所以自带了相对于动画的节流效果!如果你的主线程有动画之类的卡了一点,事件触发频率非常可能低于16ms。我在最开始关于渲染时机的内容中说了scroll和resize因为和渲染处于同一轮次,所以最快也就每帧执行一次,现在来看,不仅仅是scroll和resize!连touchmove、mousemove等事件,由于Compositor Thread的机制原因,也依然如此!
    详见这个jsfiddle,大家可以试试,你可以发现mousemove回调和requestAnimationFrame回调的调用频率是完全一致的,mousemove的执行次数跟raf执行次数一模一样,永远没有任何一次出现mousemove执行两次而rAF还没有执行一次的情况发生。另外两次执行间隔在14到20毫秒之间,主要是因为帧的间隔不会精确到16.666毫秒哈,基本是14ms~20ms之间大致波动的,大家可以打开timeline观察。另外有个挺奇怪的现象是每次鼠标从devtool移回页面区域里的时候,会非常快的触发两次mousemove(间隔有时小于5ms),虽然依然每次mousemove后依然紧跟raf,这意味着非常快速的触发了两帧。

  3. requestAnimationFrame
    图中的红线的意思是你可能会在JS里Force Layout,也就是我们说的访问了scrollWidth、clientHeight、ComputedStyle等触发了强制重排,导致Recalc Styles和Layout前移到代码执行过程当中。
  4. parse HTML
    如果有DOM变动,那么会有解析DOM的这一过程。
  5. Recalc Styles
    如果你在JS执行过程中修改了样式或者改动了DOM,那么便会执行这一步,重新计算指定元素及其子元素的样式。
  6. Layout
    我们常说的重排reflow。如果有涉及元素位置信息的DOM改动或者样式改动,那么浏览器会重新计算所有元素的位置、尺寸信息。而单纯修改color、background等等则不会触发重排。详见css-triggers
  7. update layer tree
    这一步实际是更新Render Layer的层叠排序关系,也就是我们之前说的为了搞定层叠上下文搞出的那个东西,因为之前更新了相关样式信息和重排,所以层叠情况也可能变动。
  8. Paint
    其实Paint有两步,第一步是记录要执行哪些绘画调用,第二步才是执行这些绘画调用。第一步只是把所需要进行的操作记录序列化进一个叫做SkPicture的数据结构里:

    The SkPicture is a serializable data structure that can capture and then later replay commands, similar to a display list.

    这个SkPicture其实就一个列表,记录了你的commands。接下来的第二步里会将SkPicture中的操作replay出来,这里才是将这些操作真正执行:光栅化和填充进位图。主线程中和我们在Timeline中看到的这个Paint其实是Paint的第一步操作。第二步是后续的Rasterize步骤(见后文)。

  9. Composite
    主线程里的这一步会计算出每个Graphics Layers的合成时所需要的data,包括位移(Translation)、缩放(Scale)、旋转(Rotation)、Alpha 混合等操作的参数,并把这些内容传给Compositor Thread,然后就是图中我们看到的第一个commit:Main Thread告诉Compositor Thread,我搞定了,你接手吧。然后主线程此时会去执行requestIdleCallback。这一步并没有真正对Graphics Layers完成位图的composite。
  10. Raster Scheduled and Rasterize
    第8步生成的SkPicture records在这个阶段被执行。

    SkPicture records on the compositor thread get turned into bitmaps on the GPU in one of two ways: either painted by Skia’s software rasterizer into a bitmap and uploaded to the GPU as a texture, or painted by Skia’s OpenGL backend (Ganesh) directly into textures on the GPU.

    可以看出Rasterization其实有两种形式:

    • 一种是基于CPU、使用Skia库的Software Rasterization,首先绘制进位图里,然后再作为纹理上传至GPU。这一方式中,Compositor Thread会spawn出一个或多个Compositor Tile Worker Thread,然后多线程并行执行SkPicture records中的绘画操作,以之前介绍的Graphics Layer为单位,绘制Graphics Layer里的Render Object。同时这一过程是将Layer拆分为多个小tile进行光栅化后写入进tile对应的位图中的。
    • 另一种则是基于GPU的Hardware Rasterization,也是基于Compositor Tile Worker Thread,也是分tile进行,但是这个过程不是像Software Rasterization那样在CPU里绘制到位图里,然后再上传到GPU中作为纹理。而是借助Skia’s OpenGL backend (Ganesh) 直接在GPU中的纹理中进行绘画和光栅化,填充像素。也就是我们常说的GPU Raster。

    现在基本最新版的几大浏览器都是硬件Rasterization了,但是对于一些移动端基本还是Software Rasterization较多。打开你的chrome浏览器输入chrome://gpu/ 可以看看你的chrome的GPU加速情况。下图是我的:

    使用Hardware Rasterization的好处在于:以往Software Rasterization的方式,受限于CPU和GPU之前的上传带宽,把位图从RAM里上传到GPU的VRAM里的过程是有不可忽视的性能开销的。若Rasterization的区域较大,那么使用Software Rasterization很可能在这里出现卡顿。下面这个例子是Chrome32和Chrome41的对比,后者的版本实现了Hardware Rasterization。

    不过,对于图片、canvas等情况,我没有查到到底是怎么处理的,但是我觉得绝对是有一个从CPU上传到GPU的过程的,所以应该有一些情况不是纯Hardware Rasterization的,两者应该是结合使用的。另外就是硬件还是软件Rasterization主要还是由设备决定的,在这个地方并没有我们手动优化的空间,但是这里涉及到一些后面的内容,所以简单介绍了一下。

  11. commit
    如果是Software Rasterization,所有tile的光栅化完成后Compositor Thread会commit通知GPU Thread,于是所有的tile的位图都会作为纹理都会被GPU Thread上传到GPU里。如果是使用GPU 的Hardware Rasterization,那么此时纹理都已经在GPU中。接下来,GPU Thread会调用平台对应的3D API(windows下是D3D,其他平台都是GL),把所有纹理绘制到最终的一个位图里,从而完成纹理的合并。
    同时,非常关键的一点:在纹理的合并时,借助于3D API的相关合成参数,可以在合并前对纹理transformations(也就是之前提到的位移、旋转、缩放、alpha通道改变等等操作),先变形再合并。合并完成之后就可以将内容呈现到屏幕上了。

并不是每次渲染都会执行上述11步的所有步骤,比如Layout、Paint、Rasterize、commit可能一次都没有,但是Layout又可能会不止一次。另外还有利用合成层提升来获得GPU加速的动画等相关技术的原理。接下里就是对上述步骤更加详细的分析。

重排 Layout、强制重排 Force Layout

重排和强制重排是老生常谈的东西了,大家也应该非常熟悉了,但在这里可以结合浏览器机制顺带讲一遍。

首先,如果你改了一个影响元素布局信息的CSS样式,比如width、height、left、top等(transform除外),那么浏览器会将当前的Layout标记为dirty,这会使得浏览器在下一帧执行上述11个步骤的时候执行Layout。因为元素的位置信息变了,将可能会导致整个网页其他元素的位置情况都发生改变,所以需要执行Layout全局重新计算每个元素的位置。

需要注意到,浏览器是在下一帧、下一次渲染的时候才重排。并不是JS执行完这一行改变样式的语句之后立即重排,所以你可以在JS语句里写100行改CSS的语句,但是只会在下一帧的时候重排一次。

如果你在当前Layout被标记为dirty的情况下,访问了offsetTop、scrollHeight等属性,那么,浏览器会立即重新Layout,计算出此时元素正确的位置信息,以保证你在JS里获取到的offsetTop、scrollHeight等是正确的。

会触发重排的属性和方法:

这一过程被称为强制重排 Force Layout,这一过程强制浏览器将本来在上述渲染流程中才执行的Layout过程前提至JS执行过程中。前提不是问题,问题在于每次你在Layout为dirty时访问会触发重排的属性,都会Force Layout,这极大的延缓了JS的执行效率。

这三行代码的后两行都导致了Force Layout,Layout一次的时间视DOM数量级从几十微秒到十几毫秒不等,相比于一行JS 1微秒不到的执行时间,这个开销是难以接受的。所以也就有了读写分离、纯用变量存储等避免Force Layout的方法。否则你就会在你Timeline里看到这种10多次Recalculate Style 和 Layout的画面了。

另外,每次重排或者强制重排后,当前Layout就不再dirty。所以你再访问offsetWidth之类的属性,并不会再触发重排。

重绘 Paint

重绘也是相似的,一旦你更改了某个元素的会触发重绘的样式,那么浏览器就会在下一帧的渲染步骤中进行重绘。也即一些介绍重绘机制中说的invalidating(作废),JS更改样式导致某一片区域的样式作废,从而在一下帧中重绘invalidating的区域。

但是,有一个非常关键的行为,就是:重绘是以合成层为单位的。也即 invalidating的既不是整个文档,也不是单个元素,而是这个元素所在的合成层。当然,这也是将渲染过程拆分为Paint和Compositing的初衷之一:

Since painting of the layers is decoupled from compositing, invalidating one of these layers only results in repainting the contents of that layer alone and recompositing.

这里给出两个demo: demo1 和 demo2

两个demo几乎完全一样,除了第二demo的.ab-right的样式里多了一行,will-change:transform;我们在前文介绍合成层的时候强调过will-change: transform会让元素强制提升为合成层。

于是在第二个demo中出现了两个合成层:HTML根元素的合成层和.ab-right所在的合成层。

然后我们在js中修改了#target元素的样式,于是#target元素在的合成层(即HTML根元素的合成层)被重绘。在demo1中,.ab-right元素没有被提升为合成层,于是.ab-right也被重绘了。而在demo2中,.ab-right元素并没有重绘。先看demo1:


明显的看到.ab-right被重绘了。


显然,demo2只重绘了HTML根元素的合成层的内容。

对了,你还可以顺便点到Raster一栏去看看Rasterization的具体过程。前面已经介绍过了,这里真正完成Paint里的操作,将内容绘制进位图或纹理中,且是分tile进行的。

重排和重绘和Compositing

先说点题外的,怎么查看合成层:

修改一些CSS属性如width、float、border、position、font-size、text-align、overflow-y等等会触发重排、重绘和合成,修改另一些属性如color、background-color、visibility、text-decoration等等则不会触发重排,只会重绘和合成,具体属性列表请自行google。

接下来很多文章里就会说,修改opacity、transform这两个属性仅仅会触发合成,不会触发重绘和合成。所以一定要用这两个属性来实现动画,没有重绘重排,效率很高。

然而事实并不是这样。

只有一个元素在被提升为合成层之后,上述情况才成立。

回到我们之前说的渲染过程的第11步:

同时,非常关键的一点:在纹理的合并时,借助于3D API的相关合成参数,可以在合并前对纹理transformations(也就是之前提到的位移、旋转、缩放、alpha通道改变等等操作),先变形再合并。合并完成之后就可以将内容呈现到屏幕上了。

在合成多个合成层时,确实可以借助3D API的相关参数,从而直接实现合成层的transform、opacity效果。所以如果你将一个元素提升为合成层,然后用JS修改其transform或opacity 或者在 transform或opacity 上施加CSS过渡或动画,确实会避免CPU的Paint过程,因为transform和opacity可以直接基于GPU的合成参数来完成。

但是,这是在合成层整体有transform或opacity才会这么做。对于没有提升为合成层的元素,仅仅是他自己具有transform和opacity,他是作为合成层的内容。而生成合成层的内容和写进位图或纹理是在Paint和Rasterize阶段完成的,因此这个元素的transform和opacity的实现也是在Paint和Rasterize中完成的。所以还是会重排,也就没有启用我们常说的GPU加速的动画。

比如这个demo,一个提升为合成层的div#father和一个未提升合成层的div#child,3秒钟后JS更改child和father的transform属性。 接下来渲染的时候流程是怎样的?

  1. Recalc Styles(重新计算样式)
  2. Paint 绘制变动的合成层 即 div#father
    1. Paint 绘制父元素的背景和textNode(即”父元素 提升为合成层”)
    2. Paint 绘制child元素 即div#child
      1. Paint 先translate,完成移动
      2. Paint 再在移动后的区域里绘制子元素的背景和textNode(即”子元素 未提升为合成层”)
  3. Rasterize
  4. Composite 合并合成层,在合成时借助于3D API的相关合成参数完成合成层的位移、旋转等变换,所以div#father的translate在这里实现

所以我们看到了,对于未提升合成层的元素,他的transform、opacity等是在主线程里Paint和配合Rasterize来实现的(其他的需要重绘的属性更是如此),依然会触发重绘,直接用JS改动这俩属性并不会获得性能提升。而如果元素已提升为合成层,那么他的transform、opacity等样式的实现就是直接由GPU Thread控制在GPU中Compositing来完成的,主线程的Composite步骤只是计算出合成的参数,耗时极小,速度极快,所以因此就有了尽量使用transform和opacity来完成动画的经验之谈。

借用这篇文章中的例子:

这段transition的实现过程是这样的:

而如果代码变成了这样


也就是Main Thread不用重排,不用重绘,Draw也不是他完成的,他的Composite步骤只是计算出具体的Compositing参数而已(示例中其实右边应该是Compositor和GPU Thread,但是作者为了简化概念、便于阐述,直接就没有提GPU Thread,大家不要在此处扣细节)。

另外,第二个例子中div为什么提升为合成层,其实就是前文介绍合成层的时候说的:

对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)

括号中的内容也很关键,元素在opacity等属性具有动画时,并不是直接就提升为合成层,而是动画或者transition开始时才提升为合成层,并且结束后提升合成层也失效。
同时,元素在提升为合成层或者提升合成层失效时,会触发重绘。这也是上图一开始在动画开始前有Layout the element first time 和Paint the element into the bitmap两步的原因:transition开始前,div并未被提升为合成层,transition开始,div立马提升合成层,立马导致其本来所在的合成层重绘(因为要剔除掉提升为合成层的div),并且div因为提升为合成层,也立马重绘,两个重绘好的合成层Rasterize后上传至GPU中。

demo在此,所以在动画开始前看到:

在动画结束后的那一帧则是这样:

这个上述demo中,只有2个dom,所以Paint开销几乎可以忽略,但是如果是dom数量多一些,那么就很可能是下面这样了。

实时上这个情况不止是在动画和过渡时,只要一个元素被提升为合成层,在提升前和合成层失效时都会有这个过程,所以一方面是重绘带来了绘制开销,另外则是纹理上传过程因为CPU到GPU的带宽带来的上传开销(虽然现在已经有Hardware Raster不用上传,但是仍然有不能用Hardware Raster的情况,而且Hardware Raster绘制进纹理的绘制过程本身也是有开销的)。 因此处理不好就可能导致动画开始前和开始后出现一帧卡顿/延迟。

最后,重要的一点,也是一般谈到性能优化的文章中都会介绍的一点,即:

合成层提升并非银弹。

合成层提升一方面可能会引入纹理生成、上传和重绘的开销,而且合成层提升后会占用GPU VRAM,VRAM可并不会很大。对于移动端,上述两个问题尤甚。而且在介绍合成层时,我还介绍了合成层存在隐式提升的情况。因此请合理使用。

本文主要介绍原理,所以怎么去实现16ms的动画、怎么去提升渲染性能、怎么去优化合成层数量和避免层爆炸等等、以及到底哪些情况会提升合成层、触发重绘等详细内容还是见文末附录吧。

总结

正文算是比较详细的介绍浏览器的渲染过程,可能需要你事先理解重绘、重排和合成,结合了一些demo,深入了一些我之前理解错的点。

这里再次强调一下一些颠覆了我认知的内容:

  • 按照HTML5标准,scroll事件是每帧触发一次的,自带requestAnimationFrame节流效果
  • 按照Blink和Webkit引擎实现,touchmove、mousemove等UI input由Compositor线程接收,但传入到主线程是每帧一次,也自带requestAnimationFrame节流效果
  • 重绘是以合成层为单位的
  • 合成层提升前后的Paint步骤

三周前就第一次发布的文章终于在五一节的假期里搞定。呼….

参考资料

chromium官方资料

渲染机制

实操&&性能优化

浏览器渲染详细过程:重绘、重排和 composite 只是冰山一角的更多相关文章

  1. 160826、浏览器渲染页面过程描述,DOM编程技巧以及重排和重绘

    一.浏览器渲染页过程描述   1.浏览器解析html源码,然后创建一个DOM树. 在DOM树中,每一个HTML标签都有一个对应的节点(元素节点),并且每一个文本也都有一个对应的节点(文本节点). DO ...

  2. 浏览器渲染页面过程描述,DOM编程技巧以及重排和重绘。

    一.浏览器渲染页过程描述 1.浏览器解析html源码,然后创建一个DOM树. 在DOM树中,每一个HTML标签都有一个对应的节点(元素节点),并且每一个文本也都有一个对应的节点(文本节点). DOM树 ...

  3. DOM 操作成本究竟有多高,HTML、CSS构建过程 ,从什么方向出发避免重绘重排)

    前言: 2019年!我准备好了 正文:从我接触前端到现在,一直听到的一句话:操作DOM的成本很高,不要轻易去操作DOM.尤其是React.vue等MV*框架的出现,数据驱动视图的模式越发深入人心,jQ ...

  4. 关于DOM的操作以及性能优化问题-重绘重排

     写在前面: 大家都知道DOM的操作很昂贵. 然后贵在什么地方呢? 一.访问DOM元素 二.修改DOM引起的重绘重排 一.访问DOM 像书上的比喻:把DOM和JavaScript(这里指ECMScri ...

  5. 前端性能优化--为什么DOM操作慢? 浅谈DOM的操作以及性能优化问题-重绘重排 为什么要减少DOM操作 为什么要减少操作DOM

    前端性能优化--为什么DOM操作慢?   作为一个前端,不能不考虑性能问题.对于大多数前端来说,性能优化的方法可能包括以下这些: 减少HTTP请求(合并css.js,雪碧图/base64图片) 压缩( ...

  6. js 性能篇--dom 重绘 重排 节流

    浏览器下载完页面中的所有组件----HTML标记,Js,CSS,图片等之后会解析并生成两个内部数据结构: DOM树  -------- 表示页面结构 渲染树   -------- 表示DOM节点如何显 ...

  7. Dom 重绘重排

    https://juejin.im/entry/590801780ce46300617c89b8   DOM 重绘重排

  8. Reflow(渲染)和Repaint(重绘)

    Reflow(渲染):对于DOM结构中的各个元素都有自己的盒模型,浏览器根据各种样式(浏览器的.开发人员定义的等)来计算,并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow. refl ...

  9. 浏览器的回流与重绘 (Reflow & Repaint)

    写在前面 在讨论回流与重绘之前,我们要知道: 浏览器使用流式布局模型 (Flow Based Layout). 浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了 ...

随机推荐

  1. Git命令之tag差异比较

    比较两个tag的区别有以下几种方式: 例如两个tag分别为v1.0. v2.0 1. git log v2.0 ^v1.0 查看v2.0上有,而v1.0上没有的部分: 2. git log v1.0. ...

  2. fp-growth代码问题(Python)

    网上的 python3 fp-growth代码每次在执行时可能会出现找出的频繁项集不一致的情况,这是因为每次执行代码时建的FP树可能不一致. 加了一行代码可以解决这个问题(第59行):先对 frequ ...

  3. [转帖]自动交互式脚本--expect

    自动交互式脚本--expect https://www.cnblogs.com/zhuiluoyu/p/4873869.html 我们经常会遇到一些需要与服务器程序打交道的场景,比如,从登陆某个服务器 ...

  4. c++作业题sin公式

    今日 有一位同样读大一的朋友向我求助有关c++的作业题 他说他的程序逻辑正确 但是结果的精度不对 题目如下: 这是一道看起来十分简单的作业题 我按照要求快速地写了一个版本 不出所料 一样遇到了精度问题 ...

  5. Qt元对象(Meta-Object)系统与反射

    反射 -在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问.检测和修改它本身状态或行为的一种能力.[1]用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为. 要注意 ...

  6. [转] service worker初探:超级拦截器与预缓存

    在2014年,W3C公布了service worker的草案,service worker提供了很多新的能力,使得web app拥有与native app相同的离线体验.消息推送体验. service ...

  7. java什么是构造方法

    构造方法 一.构造方法的特点 (1)每当创建给定类的实例时就调用的方法 (2)与类同名,但没有返回类型 (3)Java 为对象分配内存,初始化实例变量并调用构造方法 (4)两种构造方法 1.参数化构造 ...

  8. ASP.NET Core应用程序容器化、持续集成与Kubernetes集群部署(一)(转载)

    本文结构 ASP.NET Core应用程序的构建 ASP.NET Core应用程序容器化所需注意的问题 应用程序的配置信息 端口侦听 ASP.NET Core的容器版本 docker镜像构建上下文(B ...

  9. 创建一个dotnetcore的SPA模板项目

    MPA和SPA 对比 dotnet new --install Microsoft.AspNetCore.SpaTemplates::* 我这边用的是vue dotnet new vue dotnet ...

  10. jQuery实现form表单基于ajax无刷新提交方法详解

    本文实例讲述了jQuery实现form表单基于ajax无刷新提交方法.分享给大家供大家参考,具体如下: 首先,新建Login.html页面: <!DOCTYPE html PUBLIC &quo ...