更多精彩内容,欢迎关注作者微信大众号:码工笔记

本文首要是对Google Chrome团队在2020年共享的《Life of a Pixel》PPT 的学习和总结。这篇共享首要介绍的是Chrome将HTML烘托成屏幕像素点的中心烘托流程。虽然Chrome的架构近年发生了一些迭代,但这篇讲的相对比较清楚,每次看这个PPT都还是会有一些新的收获。

一、烘托的目标

烘托的目标是将网页内容烘托成屏幕上显现的像素点。

网页内容例如以下纽约时报的主页:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

待烘托的页面中或许包括以下多种不同类型的数据:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

而屏幕上要显现的像素点,来历于GPU驱动收到的烘托指令和数据,包括纹路、shader、vertext buffer(存了三角形顶点坐标)等:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

除此之外,因为烘托是一个持续运转的流程,除了初次烘托外,后续还会有不断的烘托更新。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

所以从性能动身,就要求烘托流程中要构建对应的内部数据结构,使得当内容有改动时能高效地更新烘托成果,其间内容更新的来历首要有:

  • JavaScript
  • 用户输入
  • 异步加载
  • 动画
  • 滚动(Scrolling)
  • 缩放

二、烘托根本流程

从概念上说,烘托能够简单抽象为以下流程:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

网页内容数据经过一系列中心处理阶段(并顺带生成了一些中心成果的数据结构),最终生成了屏幕上可见的像素点。

下面,咱们来逐渐分析烘托流程中的这些处理阶段和相应的数据结构。

1、解析DOM树

这个阶段首要是解析HTML文本,并生成DOM树:

  • 输入:HTML文本
  • 输出:DOM(Domain Object Model)树

Chrome渲染流程初探——《Life of a Pixel》学习笔记

DOM树既是Chrome的内部数据结构,也是暴露给JavaScript的API:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

2、款式表处理(Style)

款式表用于指定DOM结点的显现款式。如下图中的例子指定了一切<p>结点的文字为红色。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

一些常见的简单款式如下:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

另外,款式表也支持一些比较杂乱的声明方式:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

对款式表处理的第一步,是CSSParser模块对款式表文本进行解析,并生成StyleSheetContents目标。

StyleSheetContents目标中记录了一切声明的css规矩(StyleRule),每条StyleRule规矩都包括了其对应的挑选器(CSSSelectorList)和特点值(CSSPropertyValueSet):

Chrome渲染流程初探——《Life of a Pixel》学习笔记

然后就是对DOM结点进行款式核算,这一步会核算出DOM树中一切结点的一切显现款式。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

能够在JavaScript中运用getComputedStyle(element)得到element的款式核算成果:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

3、布局(Layout)

接下来是布局阶段。

布局是要核算出各个DOM结点的坐标和宽高信息。如下图示:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

以flow布局为例,flow布局首要分两种:

  • block flow:这种布局比较简单,就是将待布局目标在笔直方向上次序排列:

    Chrome渲染流程初探——《Life of a Pixel》学习笔记

  • inline flow:这种布局杂乱一些,文本或”inline”元素从左到右排布,超出边界时会折行:

    Chrome渲染流程初探——《Life of a Pixel》学习笔记

布局器需求依据字体信息对文本的高度和宽度进行测量,其间Shaper模块担任挑选glyph并核算其详细放置方位:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

有些布局目标的内容或许会超出(overflow)它的边框范围,对于overflow的部分,用户能够设置显现、隐藏或scroll:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

其他布局(如flex)的处理流程或许更杂乱:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

布局树

布局器会依据DOM树结构出一棵布局树,在核算布局时会遍历布局树,核算每个布局目标的坐标和宽高。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

DOM树与布局树中的结点并不是一一对应的:有的DOM结点没有对应的布局结点(如display:none的DOM结点),有的布局结点没有对应的DOM结点(如一些专用于布局内部逻辑的特别结点)。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

布局流程举例

以下为一段HTML文本和其对应的DOM树:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

其布局树为:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

布局核算成果反映在片段树(fragment tree)中:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

文本“The”在片段树中的对应结点:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

文本“jumps”在片段树中的对应结点:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

4、烘托(paint)

接下来,是将布局成果转换成烘托指令(Paint op)。烘托指令指明晰需求在什么方位画什么元素(但不包括图片解码)。

  • 输入:布局树上的布局目标(LayoutObject)
  • 输出:一系列烘托指令

布局目标(LayoutObject)有个Paint办法,能够生成一系列烘托指令(包括了其子结点递归生成的烘托指令):

Chrome渲染流程初探——《Life of a Pixel》学习笔记

生成烘托指令时对结点的遍历是以“stacking次序”来的,而不是以DOM中结点的次序。如下图中,黄色元素的z-index较大,需求后烘托,虽然它在DOM树中排在绿色元素的前面。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

烘托流程整体上可分为多个阶段(简化版),别离烘托各元素的不同部分:

  • background
  • float
  • foreground
  • outline

每个阶段都需求别离对一切“stacking context”进行一次遍历。如下图示,布景是先烘托绿色再烘托蓝色,但绿色元素中的文字需求在蓝色布景烘托完结之后再进行烘托。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

下图中的<p>元素需求先画布景(包括布景色和边框),再画前景(文字)。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

其间文字的烘托指令内部又记录了各glyph的详细烘托细节信息(后续交给Skia烘托引擎进行烘托)。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

5、光栅化(rasterization)

下一步,就是把上面生成的烘托指令列表转换成位图(bitmap)并存到GPU memory 中(并未显现)。此过程中包括了对图片的解码操作。

  • 输入:烘托指令列表
  • 输出:位图,并存到GPU memory中(并未显现)

Chrome渲染流程初探——《Life of a Pixel》学习笔记

若指令中有图片文件,则需求对图片进行解码。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

光栅化流程能够运用GPU机制进行加快,如将需求复用的图片以纹路方式存到GPU的memory中,烘托指令中存储对应的texture ID。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

光栅化模块调用Skia图形库来生成Open GL指令。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

GPU进程

烘托指令是由render进程发生的,而光栅化则是运转在GPU进程中的。所以虽然概念上render进程发生的烘托指令是直接传送给GPU进程:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

但从完结上看,烘托指令是经过command buffer传送过去的:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

GPU进程在运转时动态链接本地的OpenGL库完结,在Windows渠道,还需求将OpenGL指令转换成DirectX指令。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

三、烘托更新流程

现在,咱们经过以上流程将内容成功烘托到GPU memory中了:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

但如果内容发生了改动,烘托成果需求更新时,又会发生什么呢?

例如,烘托进程生成了一些动画帧,某帧耗时比较长,导致它不能在16ms内完结烘托,界面就会发生卡顿。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

为进步烘托功率,烘托中的每个阶段都尽量复用之前缓存的中心成果,只有在必要时才从头履行。以下为各烘托阶段从头履行的触发条件。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

尽管各烘托阶段都有数据缓存和复用,但如果显现区域中的大部分像素都发生了改动,重绘和光栅化还是会带来很大开支。

如下图中对文本进行scroll时,显现区域中的一切像素都发生了改动:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

另外,如果在烘托线程上运转的JavaScript中有一些耗时办法,烘托流程也会被卡住。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

要解决上述问题,就需求引进分层和合成器了。

1、分层

烘托线程需求先将页面拆分为不同的图层(layer),将图层信息提交给合成器线程,各图层别离独立进行光栅化。在一切图层光栅化完毕后,合成器线程再将成果合并到一同。

注:下图中的impl线程即为合成器线程,因为历史原因叫它impl。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

每个图层包括一整棵子树的内容。

如下图中<p>具有一个独立的图层。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

而下图中的<div>和其孩子<p>共同具有一个图层:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

常见的需求独立图层的场景有以下几种:

  • 动画:对图层进行移动
  • 滚动:一个图层移动,一个图层clip它
  • 缩放:对图层进行缩放
    Chrome渲染流程初探——《Life of a Pixel》学习笔记

用户输入和交互事件(如scroll)也由合成器线程来处理,这能确保在烘托线程繁忙的时分,用户还能有较好的交互体验,而不会被烘托流程卡住:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

分层战略

是否为一棵子树拆分出一个独立的图层,首要取决于此子树是否满足一些触发条件(compositing triggers)。

例如,如果某结点设置了transform款式,则它会被提升为一个单独的图层:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

或者如果其设置了overflow:scroll特点,则除了将其提升为一个单独的图层外,还需求创建多个相关的特别图层,包括:main layer、scrolling contets layer、横竖scroll bar、scroll corner layer等。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

烘托线程在布局阶段完结后,将各布局目标分到不同的图层中(GraphicsLayer),然后对各个不同的图层别离进行烘托。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

合成器还能对图层设置一些烘托特点,以下为一些图层烘托特点树的例子:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

对某图层设置烘托特点,不会影响其他图层,所以重绘范围也就仅限于有特点改动的图层,然后进步烘托更新的功率。

目前图层烘托特点树的结构是在烘托流程中prepaint阶段(分层之后,烘托之前):

Chrome渲染流程初探——《Life of a Pixel》学习笔记

未来,Chrome新架构中,会将合成器放到烘托阶段之后,也即合成器的输入变成了烘托指令,而不是布局目标。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

2、提交(Commit)

烘托线程完结Paint阶段后,需求将烘托成果同步地提交给合成器线程,并将图层和特点树拷贝一份用于合成器线程的后续处理。在合成器线程处理完此次提交(Commit)后,烘托线程才干持续处理其他烘托任务。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

3、图层分割成瓦片(tiling)

接下来,合成器线程需求将图层分割成比较小的瓦片,并对各瓦片独立进行光栅化(可并行)。

一个图层或许很大,图层中不是一切的paint op都需求立即履行,并且取决于paint op目标离显现区域的远近,需求先光栅化显现区域内及附近的,后处理离显现区域比较远的。

光栅化的首要操作发生在GPU中,成果会存到GPU memory中。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

4、图层制作

瓦片光栅化完结后,光栅化成果已存储到GPU memory中,合成器线程生成 CompositorFrame(其间记录了瓦片光栅化的元信息目标DrawQuad,而DrawQuad中记录了的光栅化成果的纹路信息)目标并将它发送给GPU进程:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

5、激活(activation)

合成器线程保护了两套图层数据,一个是当时激活的图层树,一个是等候激活(pending)的图层树。

图层制作操作的是当时激活的图层树,烘托线程发来的提交(commit)请求修改的是等候激活的图层树。

在当时激活的图层树在进行图层制作时,如果从烘托线程发来了新的commit,则新commit中的瓦片光栅化操作能够同时进行,互不干扰。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

6、显现

运转于GPU进程中viz线程中的display compositor会将多个烘托进程中发来的CompositorFrame进行合并。

Chrome渲染流程初探——《Life of a Pixel》学习笔记

然后履行CompositorFrame中的DrawQuad指令,将烘托指令和数据以command buffer的方式发送给gpu线程:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

然后再将内容烘托到到GPU双缓冲的back buffer中:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

最终,在双缓冲swap时,GPU会展现之前back buffer中存储的已烘托好的像素点,然后将内容显现到屏幕上:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

四、整体流程

回忆一下整体流程,一个像素点从开端到完毕会经历以下过程:

Chrome渲染流程初探——《Life of a Pixel》学习笔记

  • 烘托进程
    • 烘托线程:
      • DOM(从HTML文本生成DOM树)
      • 款式处理(核算元素的一切显现款式)
      • 布局(生成布局树,依据显现款式核算布局结点的坐标、宽高)
      • 分层(为布局树上的结点分配不同的图层,进步后续烘托更新功率)
      • 烘托前处理(为各图层结构图层特点树,并设置图层特点)
      • 烘托(分阶段,以stacking次序,为各图层中的布局目标生成烘托指令Paint ops)
    • 合成器线程
      • 提交(将烘托线程生成的图层和特点树数据拷贝一份,提交过程中烘托线程同步等候,提交完结后烘托线程康复履行)
      • 图层分割成瓦片(将图层分红多个瓦片,并依据瓦片离视区的远近先后进行并行异步光栅化;图层树为等候激活状态)
  • GPU进程
    • 光栅化(调用GL办法把烘托指令转化成位图,包括图片解码;瓦片光栅化成果存储在GPU memory中)
  • 烘托进程
    • 合成器线程
      • 激活(光栅化完毕后,图层树变为激活状态)
      • 图层制作(将瓦片光栅化成果元信息打包成CompositorFrame发送给GPU进程,CompositorFrame中存储了光栅化成果在GPU memory中的texture id)
  • GPU进程
    • 显现(双缓冲)

五、参考资料

  • docs.google.com/presentatio…